Compare commits

..

1 Commits

Author SHA1 Message Date
Riccardo Balbo 3db1adbb54
Fix openal-soft dead link 5 years ago
  1. 30
      .github/workflows/main.yml
  2. 29
      LICENSE
  3. 3
      README.md
  4. 10
      build.gradle
  5. 6
      common.gradle
  6. 6
      gradle.properties
  7. 2
      jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java
  8. 2
      jme3-android-native/decode.gradle
  9. 5
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  10. 11
      jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java
  11. 4
      jme3-android/src/main/java/com/jme3/app/state/MjpegFileWriter.java
  12. 2
      jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java
  13. 2
      jme3-android/src/main/java/com/jme3/input/android/AndroidJoystickJoyInput14.java
  14. 4
      jme3-android/src/main/java/com/jme3/input/android/AndroidSensorJoyInput.java
  15. 6
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java
  16. 4
      jme3-android/src/main/java/com/jme3/input/android/TouchEventPool.java
  17. 18
      jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
  18. 2
      jme3-android/src/main/java/com/jme3/texture/plugins/AndroidNativeImageLoader.java
  19. 12
      jme3-blender/build.gradle
  20. 733
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  21. 69
      jme3-blender/src/main/java/com/jme3/asset/GeneratedTextureKey.java
  22. 193
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java
  23. 767
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  24. 419
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  25. 40
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderModelLoader.java
  26. 391
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java
  27. 134
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java
  28. 400
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneContext.java
  29. 133
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneEnvelope.java
  30. 317
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java
  31. 148
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/cameras/CameraHelper.java
  32. 88
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/BoneConstraint.java
  33. 186
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java
  34. 476
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java
  35. 397
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SimulationNode.java
  36. 41
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SkeletonConstraint.java
  37. 32
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SpatialConstraint.java
  38. 165
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/VirtualTrack.java
  39. 162
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinition.java
  40. 84
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionDistLimit.java
  41. 126
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionFactory.java
  42. 236
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java
  43. 107
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLike.java
  44. 106
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLimit.java
  45. 61
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionMaintainVolume.java
  46. 34
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionNull.java
  47. 87
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLike.java
  48. 129
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLimit.java
  49. 74
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLike.java
  50. 96
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLimit.java
  51. 81
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionTransLike.java
  52. 40
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/UnsupportedConstraintDefinition.java
  53. 172
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/BezierCurve.java
  54. 159
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesHelper.java
  55. 890
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesTemporalMesh.java
  56. 77
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java
  57. 371
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java
  58. 203
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DnaBlockData.java
  59. 135
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DynamicArray.java
  60. 327
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java
  61. 213
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/FileBlockHeader.java
  62. 189
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Pointer.java
  63. 328
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Structure.java
  64. 214
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java
  65. 116
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/lights/LightHelper.java
  66. 26
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/IAlphaMask.java
  67. 366
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  68. 387
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialHelper.java
  69. 583
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/DQuaternion.java
  70. 190
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/DTransform.java
  71. 210
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Matrix.java
  72. 869
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Vector3d.java
  73. 349
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java
  74. 613
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java
  75. 305
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/IndexesLoop.java
  76. 364
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshBuffers.java
  77. 381
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  78. 93
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Point.java
  79. 767
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java
  80. 113
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java
  81. 241
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArrayModifier.java
  82. 200
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/MaskModifier.java
  83. 196
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/MirrorModifier.java
  84. 92
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/Modifier.java
  85. 117
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ModifierHelper.java
  86. 107
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ParticlesModifier.java
  87. 642
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java
  88. 54
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/TriangulateModifier.java
  89. 520
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java
  90. 365
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/Properties.java
  91. 192
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/particles/ParticlesHelper.java
  92. 401
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ColorBand.java
  93. 543
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java
  94. 157
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/DDSTexelData.java
  95. 282
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/GeneratedTexture.java
  96. 136
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageLoader.java
  97. 473
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageUtils.java
  98. 691
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TextureHelper.java
  99. 392
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TexturePixel.java
  100. 662
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TriangulatedTexture.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -66,11 +66,10 @@ jobs:
steps: steps:
- name: Clone the repo - name: Clone the repo
uses: actions/checkout@v2 uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Validate the Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build - name: Build
run: | run: |
# Build # Build
@ -94,11 +93,10 @@ jobs:
steps: steps:
- name: Clone the repo - name: Clone the repo
uses: actions/checkout@v2 uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Validate the Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build - name: Build
run: | run: |
./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \ ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
@ -131,7 +129,7 @@ jobs:
steps: steps:
- name: Clone the repo - name: Clone the repo
uses: actions/checkout@v2 uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
@ -140,8 +138,7 @@ jobs:
with: with:
java-version: ${{ matrix.jdk }} java-version: ${{ matrix.jdk }}
architecture: x64 architecture: x64
- name: Validate the Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build Natives - name: Build Natives
shell: bash shell: bash
env: env:
@ -197,7 +194,7 @@ jobs:
steps: steps:
- name: Clone the repo - name: Clone the repo
uses: actions/checkout@v2 uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
@ -236,8 +233,7 @@ jobs:
with: with:
name: linuxarm-natives name: linuxarm-natives
path: build/native path: build/native
- name: Validate the Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build Engine - name: Build Engine
shell: bash shell: bash
run: | run: |
@ -350,8 +346,6 @@ jobs:
if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- jme3-android-native/)"; fi if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- jme3-android-native/)"; fi
if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- jme3-bullet-native-android/)"; fi if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- jme3-bullet-native-android/)"; fi
if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- jme3-bullet/)"; fi if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- jme3-bullet/)"; fi
# The bulletUrl (in gradle.properties) might have changed.
if [ "$NATIVE_CHANGES" = "" ];then NATIVE_CHANGES="$(git diff-tree --name-only "$GITHUB_SHA" "$nativeSnapshot" -- gradle.properties)"; fi
fi fi
fi fi
@ -408,7 +402,7 @@ jobs:
# We need to clone everything again for uploadToMaven.sh ... # We need to clone everything again for uploadToMaven.sh ...
- name: Clone the repo - name: Clone the repo
uses: actions/checkout@v2 uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
@ -545,10 +539,10 @@ jobs:
git config --global user.name "Github Actions" git config --global user.name "Github Actions"
git config --global user.email "actions@users.noreply.github.com" git config --global user.email "actions@users.noreply.github.com"
git add . || true git add .
git commit -m "$version" || true git commit -m "$version"
branch="gh-pages" branch="gh-pages"
git push origin "$branch" --force || true git push origin "$branch" --force
fi fi

@ -1,29 +0,0 @@
Copyright (c) 2009-2020 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.

@ -22,7 +22,6 @@ The engine is used by several commercial game studios and computer-science cours
- [Spoxel (on Steam)](https://store.steampowered.com/app/746880/Spoxel/) - [Spoxel (on Steam)](https://store.steampowered.com/app/746880/Spoxel/)
- [Nine Circles of Hell (on Steam)](https://store.steampowered.com/app/1200600/Nine_Circles_of_Hell/) - [Nine Circles of Hell (on Steam)](https://store.steampowered.com/app/1200600/Nine_Circles_of_Hell/)
- [Leap](https://gamejolt.com/games/leap/313308) - [Leap](https://gamejolt.com/games/leap/313308)
- [Jumping Jack Flag](http://timealias.bplaced.net/jack/)
## Getting started ## Getting started
@ -37,7 +36,7 @@ Note: The master branch on GitHub is a development version of the engine and is
- NetBeans Platform - NetBeans Platform
- Gradle - Gradle
Plus a bunch of awesome libraries & tight integrations like Bullet, NiftyGUI and other goodies. Plus a bunch of awesome libraries & tight integrations like Bullet, Blender, NiftyGUI and other goodies.
### Documentation ### Documentation

@ -9,7 +9,6 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
classpath 'me.tatarka:gradle-retrolambda:3.7.1'
} }
} }
@ -23,8 +22,6 @@ allprojects {
apply plugin: 'base' apply plugin: 'base'
apply from: file('version.gradle') apply from: file('version.gradle')
apply plugin: 'me.tatarka.retrolambda'
// This is applied to all sub projects // This is applied to all sub projects
subprojects { subprojects {
if(!project.name.equals('jme3-android-examples')) { if(!project.name.equals('jme3-android-examples')) {
@ -254,10 +251,3 @@ if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
wrapper { wrapper {
gradleVersion = '5.6.4' gradleVersion = '5.6.4'
} }
retrolambda {
javaVersion JavaVersion.VERSION_1_7
incremental true
jvmArgs '-noverify'
}

@ -12,12 +12,6 @@ version = jmeFullVersion
sourceCompatibility = '1.8' sourceCompatibility = '1.8'
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
gradle.projectsEvaluated {
tasks.withType(JavaCompile) { // compile-time options:
options.compilerArgs << '-Xlint:unchecked'
}
}
repositories { repositories {
mavenCentral() mavenCentral()
maven { maven {

@ -27,9 +27,9 @@ skipPrebuildLibraries=false
ndkPath = /opt/android-ndk-r16b ndkPath = /opt/android-ndk-r16b
# Path for downloading native Bullet # Path for downloading native Bullet
# 2.89+ (circa 26 April 2020, to avoid jMonkeyEngine issue #1283) # 2.88+
bulletUrl = https://github.com/bulletphysics/bullet3/archive/cd8cf7521cbb8b7808126a6adebd47bb83ea166a.zip bulletUrl = https://github.com/bulletphysics/bullet3/archive/28039903b14c2aec28beff5b3609c9308b17e76a.zip
bulletFolder = bullet3-cd8cf7521cbb8b7808126a6adebd47bb83ea166a bulletFolder = bullet3-28039903b14c2aec28beff5b3609c9308b17e76a
bulletZipFile = bullet3.zip bulletZipFile = bullet3.zip
# POM settings # POM settings

@ -286,7 +286,7 @@ public class MainActivity extends AppCompatActivity implements OnItemClickListen
private boolean checkClassType(String className) { private boolean checkClassType(String className) {
boolean include = true; boolean include = true;
try { try {
Class<?> clazz = Class.forName(className); Class<?> clazz = (Class<?>) Class.forName(className);
if (Application.class.isAssignableFrom(clazz)) { if (Application.class.isAssignableFrom(clazz)) {
Log.d(TAG, "Class " + className + " is a jME Application"); Log.d(TAG, "Class " + className + " is a jME Application");
} else { } else {

@ -1,5 +1,5 @@
String tremorZipFile = "TremorAndroid.zip" String tremorZipFile = "TremorAndroid.zip"
String stbiUrl = 'https://raw.githubusercontent.com/jMonkeyEngine/stb/0224a44a10564a214595797b4c88323f79a5f934/stb_image.h' String stbiUrl = 'https://raw.githubusercontent.com/nothings/stb/master/stb_image.h'
// Working directories for the ndk build. // Working directories for the ndk build.
String decodeBuildDir = "${buildDir}" + File.separator + 'decode' String decodeBuildDir = "${buildDir}" + File.separator + 'decode'

@ -239,8 +239,9 @@ public class AndroidHarness extends Activity implements TouchListener, DialogInt
// Create application instance // Create application instance
try { try {
if (app == null) { if (app == null) {
Class clazz = Class.forName(appClass); @SuppressWarnings("unchecked")
app = (LegacyApplication)clazz.newInstance(); Class<? extends LegacyApplication> clazz = (Class<? extends LegacyApplication>) Class.forName(appClass);
app = clazz.newInstance();
} }
app.setSettings(settings); app.setSettings(settings);

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2009-2020 jMonkeyEngine * Copyright (c) 2009-2019 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
@ -257,8 +257,9 @@ public class AndroidHarnessFragment extends Fragment implements
// Create application instance // Create application instance
try { try {
if (app == null) { if (app == null) {
Class clazz = Class.forName(appClass); @SuppressWarnings("unchecked")
app = (LegacyApplication)clazz.newInstance(); Class<? extends LegacyApplication> clazz = (Class<? extends LegacyApplication>) Class.forName(appClass);
app = clazz.newInstance();
} }
app.setSettings(settings); app.setSettings(settings);
@ -683,10 +684,10 @@ public class AndroidHarnessFragment extends Fragment implements
if (viewWidth > viewHeight && viewWidth > maxResolutionDimension) { if (viewWidth > viewHeight && viewWidth > maxResolutionDimension) {
// landscape // landscape
fixedSizeWidth = maxResolutionDimension; fixedSizeWidth = maxResolutionDimension;
fixedSizeHeight = (int)(maxResolutionDimension * (viewHeight / (float)viewWidth)); fixedSizeHeight = (int)(maxResolutionDimension * ((float)viewHeight / (float)viewWidth));
} else if (viewHeight > viewWidth && viewHeight > maxResolutionDimension) { } else if (viewHeight > viewWidth && viewHeight > maxResolutionDimension) {
// portrait // portrait
fixedSizeWidth = (int)(maxResolutionDimension * (viewWidth / (float)viewHeight)); fixedSizeWidth = (int)(maxResolutionDimension * ((float)viewWidth / (float)viewHeight));
fixedSizeHeight = maxResolutionDimension; fixedSizeHeight = maxResolutionDimension;
} else if (viewWidth == viewHeight && viewWidth > maxResolutionDimension) { } else if (viewWidth == viewHeight && viewWidth > maxResolutionDimension) {
fixedSizeWidth = maxResolutionDimension; fixedSizeWidth = maxResolutionDimension;

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2009-2020 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
@ -478,7 +478,7 @@ public class MjpegFileWriter {
baos.write(fcc); baos.write(fcc);
baos.write(intBytes(swapInt(cb))); baos.write(intBytes(swapInt(cb)));
for (int i = 0; i < ind.size(); i++) { for (int i = 0; i < ind.size(); i++) {
AVIIndex in = ind.get(i); AVIIndex in = (AVIIndex) ind.get(i);
baos.write(in.toBytes()); baos.write(in.toBytes());
} }

@ -358,7 +358,7 @@ public class VideoRecorderAppState extends AbstractAppState {
@Override @Override
public float getTimePerFrame() { public float getTimePerFrame() {
return 1.0f / this.framerate; return (float) (1.0f / this.framerate);
} }
@Override @Override

@ -113,7 +113,7 @@ public class AndroidJoystickJoyInput14 {
joysticks.clear(); joysticks.clear();
joystickIndex.clear(); joystickIndex.clear();
ArrayList<Integer> gameControllerDeviceIds = new ArrayList<>(); ArrayList gameControllerDeviceIds = new ArrayList();
int[] deviceIds = InputDevice.getDeviceIds(); int[] deviceIds = InputDevice.getDeviceIds();
for (int deviceId : deviceIds) { for (int deviceId : deviceIds) {
InputDevice dev = InputDevice.getDevice(deviceId); InputDevice dev = InputDevice.getDevice(deviceId);

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2009-2020 jMonkeyEngine * Copyright (c) 2009-2018 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
@ -561,7 +561,7 @@ public class AndroidSensorJoyInput implements SensorEventListener {
} }
} }
} }
} else { } else if (sensorData != null) {
if (!sensorData.haveData) { if (!sensorData.haveData) {
sensorData.haveData = true; sensorData.haveData = true;
} }

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2009-2020 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
@ -131,8 +131,8 @@ public class AndroidTouchInput implements TouchInput {
// view width and height are 0 until the view is displayed on the screen // view width and height are 0 until the view is displayed on the screen
if (androidInput.getView().getWidth() != 0 && androidInput.getView().getHeight() != 0) { if (androidInput.getView().getWidth() != 0 && androidInput.getView().getHeight() != 0) {
scaleX = settings.getWidth() / (float)androidInput.getView().getWidth(); scaleX = (float)settings.getWidth() / (float)androidInput.getView().getWidth();
scaleY = settings.getHeight() / (float)androidInput.getView().getHeight(); scaleY = (float)settings.getHeight() / (float)androidInput.getView().getHeight();
} }
logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}", logger.log(Level.FINE, "Setting input scaling, scaleX: {0}, scaleY: {1}",
new Object[]{scaleX, scaleY}); new Object[]{scaleX, scaleY});

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2009-2020 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
@ -87,7 +87,7 @@ public class TouchEventPool {
TouchEvent evt = null; TouchEvent evt = null;
int curSize = eventPool.size(); int curSize = eventPool.size();
while (curSize > 0) { while (curSize > 0) {
evt = eventPool.pop(); evt = (TouchEvent)eventPool.pop();
if (evt.isConsumed()) { if (evt.isConsumed()) {
break; break;
} else { } else {

@ -52,7 +52,13 @@ 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.*; import com.jme3.renderer.opengl.GL;
import com.jme3.renderer.opengl.GLES_30;
import com.jme3.renderer.opengl.GLDebugES;
import com.jme3.renderer.opengl.GLExt;
import com.jme3.renderer.opengl.GLFbo;
import com.jme3.renderer.opengl.GLRenderer;
import com.jme3.renderer.opengl.GLTracer;
import com.jme3.system.*; import com.jme3.system.*;
import com.jme3.util.AndroidBufferAllocator; import com.jme3.util.AndroidBufferAllocator;
import com.jme3.util.BufferAllocatorFactory; import com.jme3.util.BufferAllocatorFactory;
@ -203,14 +209,14 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
}); });
timer = new NanoTimer(); timer = new NanoTimer();
GL gl = new AndroidGL(); Object gl = new AndroidGL();
if (settings.getBoolean("GraphicsDebug")) { if (settings.getBoolean("GraphicsDebug")) {
gl = (GL) GLDebug.createProxy(gl, gl, GL.class, GL2.class, GLES_30.class, GLFbo.class, GLExt.class); gl = new GLDebugES((GL) gl, (GLExt) gl, (GLFbo) gl);
} }
if (settings.getBoolean("GraphicsTrace")) { if (settings.getBoolean("GraphicsTrace")) {
gl = (GL)GLTracer.createGlesTracer(gl, GL.class, GLES_30.class, GLFbo.class, GLExt.class); gl = GLTracer.createGlesTracer(gl, GL.class, GLES_30.class, GLFbo.class, GLExt.class);
} }
renderer = new GLRenderer(gl, (GLExt)gl, (GLFbo)gl); renderer = new GLRenderer((GL)gl, (GLExt)gl, (GLFbo)gl);
renderer.initialize(); renderer.initialize();
JmeSystem.setSoftTextDialogInput(this); JmeSystem.setSoftTextDialogInput(this);
@ -249,7 +255,7 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
} }
if (settings.getFrameRate() > 0) { if (settings.getFrameRate() > 0) {
minFrameDuration = (long)(1000d / settings.getFrameRate()); // ms minFrameDuration = (long)(1000d / (double)settings.getFrameRate()); // ms
logger.log(Level.FINE, "Setting min tpf: {0}ms", minFrameDuration); logger.log(Level.FINE, "Setting min tpf: {0}ms", minFrameDuration);
} else { } else {
minFrameDuration = 0; minFrameDuration = 0;

@ -31,7 +31,7 @@ public class AndroidNativeImageLoader implements AssetLoader {
InputStream in = null; InputStream in = null;
try { try {
in = info.openStream(); in = info.openStream();
return load(in, flip, tmpArray); return load(info.openStream(), flip, tmpArray);
} finally { } finally {
if (in != null){ if (in != null){
in.close(); in.close();

@ -0,0 +1,12 @@
if (!hasProperty('mainClass')) {
ext.mainClass = ''
}
dependencies {
compile project(':jme3-core')
compile project(':jme3-desktop')
compile project(':jme3-effects')
compile ('org.ejml:core:0.27')
compile ('org.ejml:dense64:0.27')
compile ('org.ejml:simple:0.27')
}

@ -0,0 +1,733 @@
/*
* Copyright (c) 2009-2018 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.asset;
import java.io.IOException;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode;
/**
* Blender key. Contains path of the blender file and its loading properties.
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderKey extends ModelKey {
protected static final int DEFAULT_FPS = 25;
/**
* FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time
* between the frames.
*/
protected int fps = DEFAULT_FPS;
/**
* This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded.
*/
protected int featuresToLoad = FeaturesToLoad.ALL;
/** 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;
/** The root path for all the assets. */
protected String assetRootPath;
/** This variable indicate if Y axis is UP axis. If not then Z is up. By default set to true. */
protected boolean fixUpAxis = true;
/** Generated textures resolution (PPU - Pixels Per Unit). */
protected int generatedTexturePPU = 128;
/**
* The name of world settings that the importer will use. If not set or specified name does not occur in the file
* then the first world settings in the file will be used.
*/
protected String usedWorld;
/**
* User's default material that is set for objects that have no material definition in blender. The default value is
* null. If the value is null the importer will use its own default material (gray color - like in blender).
*/
protected Material defaultMaterial;
/** Face cull mode. By default it is disabled. */
protected FaceCullMode faceCullMode = FaceCullMode.Back;
/**
* Variable describes which layers will be loaded. N-th bit set means N-th layer will be loaded.
* If set to -1 then the current layer will be loaded.
*/
protected int layersToLoad = -1;
/** A variable that toggles the object custom properties loading. */
protected boolean loadObjectProperties = true;
/**
* Maximum texture size. Might be dependant on the graphic card.
* This value is taken from <b>org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE</b>.
*/
protected int maxTextureSize = 8192;
/** Allows to toggle generated textures loading. Disabled by default because it very often takes too much memory and needs to be used wisely. */
protected boolean loadGeneratedTextures;
/** Tells if the mipmaps will be generated by jme or not. By default generation is dependant on the blender settings. */
protected MipmapGenerationMethod mipmapGenerationMethod = MipmapGenerationMethod.GENERATE_WHEN_NEEDED;
/**
* If the sky has only generated textures applied then they will have the following size (both width and height). If 2d textures are used then the generated
* textures will get their proper size.
*/
protected int skyGeneratedTextureSize = 1000;
/** The radius of a shape that will be used while creating the generated texture for the sky. The higher it is the larger part of the texture will be seen. */
protected float skyGeneratedTextureRadius = 1;
/** The shape against which the generated texture for the sky will be created. */
protected SkyGeneratedTextureShape skyGeneratedTextureShape = SkyGeneratedTextureShape.SPHERE;
/**
* This field tells if the importer should optimise the use of textures or not. If set to true, then textures of the same mapping type will be merged together
* and textures that in the final result will never be visible - will be discarded.
*/
protected boolean optimiseTextures;
/** The method of matching animations to skeletons. The default value is: AT_LEAST_ONE_NAME_MATCH. */
protected AnimationMatchMethod animationMatchMethod = AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH;
/** The size of points that are loaded and do not belong to any edge of the mesh. */
protected float pointsSize = 1;
/** The width of edges that are loaded from the mesh and do not belong to any face. */
protected float linesWidth = 1;
/**
* Constructor used by serialization mechanisms.
*/
public BlenderKey() {
}
/**
* Constructor. Creates a key for the given file name.
* @param name
* the name (path) of a file
*/
public BlenderKey(String name) {
super(name);
}
/**
* This method returns frames per second amount. The default value is BlenderKey.DEFAULT_FPS = 25.
* @return the frames per second amount
*/
public int getFps() {
return fps;
}
/**
* This method sets frames per second amount.
* @param fps
* the frames per second amount
*/
public void setFps(int fps) {
this.fps = fps;
}
/**
* This method returns the face cull mode.
* @return the face cull mode
*/
public FaceCullMode getFaceCullMode() {
return faceCullMode;
}
/**
* This method sets the face cull mode.
* @param faceCullMode
* the face cull mode
*/
public void setFaceCullMode(FaceCullMode faceCullMode) {
this.faceCullMode = faceCullMode;
}
/**
* This method sets layers to be loaded.
* @param layersToLoad
* layers to be loaded
*/
public void setLayersToLoad(int layersToLoad) {
this.layersToLoad = layersToLoad;
}
/**
* This method returns layers to be loaded.
* @return layers to be loaded
*/
public int getLayersToLoad() {
return layersToLoad;
}
/**
* This method sets the properies loading policy.
* By default the value is true.
* @param loadObjectProperties
* true to load properties and false to suspend their loading
*/
public void setLoadObjectProperties(boolean loadObjectProperties) {
this.loadObjectProperties = loadObjectProperties;
}
/**
* @return the current properties loading properties
*/
public boolean isLoadObjectProperties() {
return loadObjectProperties;
}
/**
* The default value for this parameter is the same as defined by: org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE.
* If by any means this is too large for user's hardware configuration use the 'setMaxTextureSize' method to change that.
* @return maximum texture size (width/height)
*/
public int getMaxTextureSize() {
return maxTextureSize;
}
/**
* This method sets the maximum texture size.
* @param maxTextureSize
* the maximum texture size
*/
public void setMaxTextureSize(int maxTextureSize) {
this.maxTextureSize = maxTextureSize;
}
/**
* This method sets the flag that toggles the generated textures loading.
* @param loadGeneratedTextures
* <b>true</b> if generated textures should be loaded and <b>false</b> otherwise
*/
public void setLoadGeneratedTextures(boolean loadGeneratedTextures) {
this.loadGeneratedTextures = loadGeneratedTextures;
}
/**
* @return tells if the generated textures should be loaded (<b>false</b> is the default value)
*/
public boolean isLoadGeneratedTextures() {
return loadGeneratedTextures;
}
/**
* Not used any more.
* This method sets the asset root path.
* @param assetRootPath
* the assets root path
*/
@Deprecated
public void setAssetRootPath(String assetRootPath) {
this.assetRootPath = assetRootPath;
}
/**
* Not used any more.
* This method returns the asset root path.
* @return the asset root path
*/
@Deprecated
public String getAssetRootPath() {
return assetRootPath;
}
/**
* This method adds features to be loaded.
* @param featuresToLoad
* bitwise flag of FeaturesToLoad interface values
*/
@Deprecated
public void includeInLoading(int featuresToLoad) {
this.featuresToLoad |= featuresToLoad;
}
/**
* This method removes features from being loaded.
* @param featuresNotToLoad
* bitwise flag of FeaturesToLoad interface values
*/
@Deprecated
public void excludeFromLoading(int featuresNotToLoad) {
featuresToLoad &= ~featuresNotToLoad;
}
@Deprecated
public boolean shouldLoad(int featureToLoad) {
return (featuresToLoad & featureToLoad) != 0;
}
/**
* This method returns bitwise value of FeaturesToLoad interface value. It describes features that will be loaded by
* the blender file loader.
* @return features that will be loaded by the blender file loader
*/
@Deprecated
public int getFeaturesToLoad() {
return featuresToLoad;
}
/**
* This method determines if unlinked assets should be loaded.
* If not then only objects on selected layers will be loaded and their assets if required.
* If yes then all assets will be loaded even if they are on inactive layers or are not linked
* to anything.
* @return <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise
*/
public boolean isLoadUnlinkedAssets() {
return loadUnlinkedAssets;
}
/**
* This method sets if unlinked assets should be loaded.
* If not then only objects on selected layers will be loaded and their assets if required.
* If yes then all assets will be loaded even if they are on inactive layers or are not linked
* to anything.
* @param loadUnlinkedAssets
* <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise
*/
public void setLoadUnlinkedAssets(boolean loadUnlinkedAssets) {
this.loadUnlinkedAssets = loadUnlinkedAssets;
}
/**
* This method sets the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By default Y
* is up axis.
* @param fixUpAxis
* the up axis state variable
*/
public void setFixUpAxis(boolean fixUpAxis) {
this.fixUpAxis = fixUpAxis;
}
/**
* This method returns the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By
* default Y is up axis.
* @return the up axis state variable
*/
public boolean isFixUpAxis() {
return fixUpAxis;
}
/**
* This method sets the generated textures resolution.
* @param generatedTexturePPU
* the generated textures resolution
*/
public void setGeneratedTexturePPU(int generatedTexturePPU) {
this.generatedTexturePPU = generatedTexturePPU;
}
/**
* @return the generated textures resolution
*/
public int getGeneratedTexturePPU() {
return generatedTexturePPU;
}
/**
* @return mipmaps generation method
*/
public MipmapGenerationMethod getMipmapGenerationMethod() {
return mipmapGenerationMethod;
}
/**
* @param mipmapGenerationMethod
* mipmaps generation method
*/
public void setMipmapGenerationMethod(MipmapGenerationMethod mipmapGenerationMethod) {
this.mipmapGenerationMethod = mipmapGenerationMethod;
}
/**
* @return the size of the generated textures for the sky (used if no flat textures are applied)
*/
public int getSkyGeneratedTextureSize() {
return skyGeneratedTextureSize;
}
/**
* @param skyGeneratedTextureSize
* the size of the generated textures for the sky (used if no flat textures are applied)
*/
public void setSkyGeneratedTextureSize(int skyGeneratedTextureSize) {
if (skyGeneratedTextureSize <= 0) {
throw new IllegalArgumentException("The texture size must be a positive value (the value given as a parameter: " + skyGeneratedTextureSize + ")!");
}
this.skyGeneratedTextureSize = skyGeneratedTextureSize;
}
/**
* @return the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen
*/
public float getSkyGeneratedTextureRadius() {
return skyGeneratedTextureRadius;
}
/**
* @param skyGeneratedTextureRadius
* the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen
*/
public void setSkyGeneratedTextureRadius(float skyGeneratedTextureRadius) {
this.skyGeneratedTextureRadius = skyGeneratedTextureRadius;
}
/**
* @return the shape against which the generated texture for the sky will be created (by default it is a sphere).
*/
public SkyGeneratedTextureShape getSkyGeneratedTextureShape() {
return skyGeneratedTextureShape;
}
/**
* @param skyGeneratedTextureShape
* the shape against which the generated texture for the sky will be created
*/
public void setSkyGeneratedTextureShape(SkyGeneratedTextureShape skyGeneratedTextureShape) {
if (skyGeneratedTextureShape == null) {
throw new IllegalArgumentException("The sky generated shape type cannot be null!");
}
this.skyGeneratedTextureShape = skyGeneratedTextureShape;
}
/**
* If set to true, then textures of the same mapping type will be merged together
* and textures that in the final result will never be visible - will be discarded.
* @param optimiseTextures
* the variable that tells if the textures should be optimised or not
*/
public void setOptimiseTextures(boolean optimiseTextures) {
this.optimiseTextures = optimiseTextures;
}
/**
* @return the variable that tells if the textures should be optimised or not (by default the optimisation is disabled)
*/
public boolean isOptimiseTextures() {
return optimiseTextures;
}
/**
* Sets the way the animations will be matched with skeletons.
*
* @param animationMatchMethod
* the way the animations will be matched with skeletons
*/
public void setAnimationMatchMethod(AnimationMatchMethod animationMatchMethod) {
this.animationMatchMethod = animationMatchMethod;
}
/**
* @return the way the animations will be matched with skeletons
*/
public AnimationMatchMethod getAnimationMatchMethod() {
return animationMatchMethod;
}
/**
* @return the size of points that are loaded and do not belong to any edge of the mesh
*/
public float getPointsSize() {
return pointsSize;
}
/**
* Sets the size of points that are loaded and do not belong to any edge of the mesh.
* @param pointsSize
* The size of points that are loaded and do not belong to any edge of the mesh
*/
public void setPointsSize(float pointsSize) {
this.pointsSize = pointsSize;
}
/**
* @return the width of edges that are loaded from the mesh and do not belong to any face
*/
public float getLinesWidth() {
return linesWidth;
}
/**
* Sets the width of edges that are loaded from the mesh and do not belong to any face.
* @param linesWidth
* the width of edges that are loaded from the mesh and do not belong to any face
*/
public void setLinesWidth(float linesWidth) {
this.linesWidth = linesWidth;
}
/**
* This method sets the name of the WORLD data block that should be used during file loading. By default the name is
* not set. If no name is set or the given name does not occur in the file - the first WORLD data block will be used
* during loading (assuming any exists in the file).
* @param usedWorld
* the name of the WORLD block used during loading
*/
public void setUsedWorld(String usedWorld) {
this.usedWorld = usedWorld;
}
/**
* This method returns the name of the WORLD data block that should be used during file loading.
* @return the name of the WORLD block used during loading
*/
public String getUsedWorld() {
return usedWorld;
}
/**
* This method sets the default material for objects.
* @param defaultMaterial
* the default material
*/
public void setDefaultMaterial(Material defaultMaterial) {
this.defaultMaterial = defaultMaterial;
}
/**
* This method returns the default material.
* @return the default material
*/
public Material getDefaultMaterial() {
return defaultMaterial;
}
@Override
public void write(JmeExporter e) throws IOException {
super.write(e);
OutputCapsule oc = e.getCapsule(this);
oc.write(fps, "fps", DEFAULT_FPS);
oc.write(featuresToLoad, "features-to-load", FeaturesToLoad.ALL);
oc.write(loadUnlinkedAssets, "load-unlinked-assets", false);
oc.write(assetRootPath, "asset-root-path", null);
oc.write(fixUpAxis, "fix-up-axis", true);
oc.write(generatedTexturePPU, "generated-texture-ppu", 128);
oc.write(usedWorld, "used-world", null);
oc.write(defaultMaterial, "default-material", null);
oc.write(faceCullMode, "face-cull-mode", FaceCullMode.Off);
oc.write(layersToLoad, "layers-to-load", -1);
oc.write(mipmapGenerationMethod, "mipmap-generation-method", MipmapGenerationMethod.GENERATE_WHEN_NEEDED);
oc.write(skyGeneratedTextureSize, "sky-generated-texture-size", 1000);
oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f);
oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE);
oc.write(optimiseTextures, "optimise-textures", false);
oc.write(animationMatchMethod, "animation-match-method", AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH);
oc.write(pointsSize, "points-size", 1);
oc.write(linesWidth, "lines-width", 1);
}
@Override
public void read(JmeImporter e) throws IOException {
super.read(e);
InputCapsule ic = e.getCapsule(this);
fps = ic.readInt("fps", DEFAULT_FPS);
featuresToLoad = ic.readInt("features-to-load", FeaturesToLoad.ALL);
loadUnlinkedAssets = ic.readBoolean("load-unlinked-assets", false);
assetRootPath = ic.readString("asset-root-path", null);
fixUpAxis = ic.readBoolean("fix-up-axis", true);
generatedTexturePPU = ic.readInt("generated-texture-ppu", 128);
usedWorld = ic.readString("used-world", null);
defaultMaterial = (Material) ic.readSavable("default-material", null);
faceCullMode = ic.readEnum("face-cull-mode", FaceCullMode.class, FaceCullMode.Off);
layersToLoad = ic.readInt("layers-to=load", -1);
mipmapGenerationMethod = ic.readEnum("mipmap-generation-method", MipmapGenerationMethod.class, MipmapGenerationMethod.GENERATE_WHEN_NEEDED);
skyGeneratedTextureSize = ic.readInt("sky-generated-texture-size", 1000);
skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f);
skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE);
optimiseTextures = ic.readBoolean("optimise-textures", false);
animationMatchMethod = ic.readEnum("animation-match-method", AnimationMatchMethod.class, AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH);
pointsSize = ic.readFloat("points-size", 1);
linesWidth = ic.readFloat("lines-width", 1);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + (animationMatchMethod == null ? 0 : animationMatchMethod.hashCode());
result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode());
result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode());
result = prime * result + (faceCullMode == null ? 0 : faceCullMode.hashCode());
result = prime * result + featuresToLoad;
result = prime * result + (fixUpAxis ? 1231 : 1237);
result = prime * result + fps;
result = prime * result + generatedTexturePPU;
result = prime * result + layersToLoad;
result = prime * result + (loadGeneratedTextures ? 1231 : 1237);
result = prime * result + (loadObjectProperties ? 1231 : 1237);
result = prime * result + (loadUnlinkedAssets ? 1231 : 1237);
result = prime * result + maxTextureSize;
result = prime * result + (mipmapGenerationMethod == null ? 0 : mipmapGenerationMethod.hashCode());
result = prime * result + (optimiseTextures ? 1231 : 1237);
result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius);
result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode());
result = prime * result + skyGeneratedTextureSize;
result = prime * result + (usedWorld == null ? 0 : usedWorld.hashCode());
result = prime * result + (int) pointsSize;
result = prime * result + (int) linesWidth;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BlenderKey) {
return false;
}
BlenderKey other = (BlenderKey) obj;
if (animationMatchMethod != other.animationMatchMethod) {
return false;
}
if (assetRootPath == null) {
if (other.assetRootPath != null) {
return false;
}
} else if (!assetRootPath.equals(other.assetRootPath)) {
return false;
}
if (defaultMaterial == null) {
if (other.defaultMaterial != null) {
return false;
}
} else if (!defaultMaterial.equals(other.defaultMaterial)) {
return false;
}
if (faceCullMode != other.faceCullMode) {
return false;
}
if (featuresToLoad != other.featuresToLoad) {
return false;
}
if (fixUpAxis != other.fixUpAxis) {
return false;
}
if (fps != other.fps) {
return false;
}
if (generatedTexturePPU != other.generatedTexturePPU) {
return false;
}
if (layersToLoad != other.layersToLoad) {
return false;
}
if (loadGeneratedTextures != other.loadGeneratedTextures) {
return false;
}
if (loadObjectProperties != other.loadObjectProperties) {
return false;
}
if (loadUnlinkedAssets != other.loadUnlinkedAssets) {
return false;
}
if (maxTextureSize != other.maxTextureSize) {
return false;
}
if (mipmapGenerationMethod != other.mipmapGenerationMethod) {
return false;
}
if (optimiseTextures != other.optimiseTextures) {
return false;
}
if (Float.floatToIntBits(skyGeneratedTextureRadius) != Float.floatToIntBits(other.skyGeneratedTextureRadius)) {
return false;
}
if (skyGeneratedTextureShape != other.skyGeneratedTextureShape) {
return false;
}
if (skyGeneratedTextureSize != other.skyGeneratedTextureSize) {
return false;
}
if (usedWorld == null) {
if (other.usedWorld != null) {
return false;
}
} else if (!usedWorld.equals(other.usedWorld)) {
return false;
}
if (pointsSize != other.pointsSize) {
return false;
}
if (linesWidth != other.linesWidth) {
return false;
}
return true;
}
/**
* This enum tells the importer if the mipmaps for textures will be generated by jme. <li>NEVER_GENERATE and ALWAYS_GENERATE are quite understandable <li>GENERATE_WHEN_NEEDED is an option that checks if the texture had 'Generate mipmaps' option set in blender, mipmaps are generated only when the option is set
* @author Marcin Roguski (Kaelthas)
*/
public static enum MipmapGenerationMethod {
NEVER_GENERATE, ALWAYS_GENERATE, GENERATE_WHEN_NEEDED;
}
/**
* This interface describes the features of the scene that are to be loaded.
* @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)
*/
@Deprecated
public static interface FeaturesToLoad {
int SCENES = 0x0000FFFF;
int OBJECTS = 0x0000000B;
int ANIMATIONS = 0x00000004;
int MATERIALS = 0x00000003;
int TEXTURES = 0x00000001;
int CAMERAS = 0x00000020;
int LIGHTS = 0x00000010;
int WORLD = 0x00000040;
int ALL = 0xFFFFFFFF;
}
/**
* The shape againts which the sky generated texture will be created.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum SkyGeneratedTextureShape {
CUBE, SPHERE;
}
/**
* This enum describes which animations should be attached to which armature.
* Blender does not store the mapping between action and armature. That is why the importer
* will try to match those by comparing bone name of the armature with the channel names
* int the actions.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum AnimationMatchMethod {
/**
* Animation is matched with skeleton when at leas one bone name matches the name of the action channel.
* All the bones that do not have their corresponding channel in the animation will not get the proper tracks for
* this particulat animation.
* Also the channel will not be used for the animation if it does not find the proper bone name.
*/
AT_LEAST_ONE_NAME_MATCH,
/**
* Animation is matched when all action names are covered by the target names (bone names or the name of the
* animated spatial.
*/
ALL_NAMES_MATCH;
}
}

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

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

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

@ -0,0 +1,419 @@
/*
* Copyright (c) 2009-2019 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.animation.Animation;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;
import com.jme3.asset.BlenderKey;
import com.jme3.asset.ModelKey;
import com.jme3.asset.StreamAssetInfo;
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.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.cameras.CameraHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.curves.CurvesHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.landscape.LandscapeHelper;
import com.jme3.scene.plugins.blender.lights.LightHelper;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
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.objects.ObjectHelper;
import com.jme3.scene.plugins.blender.particles.ParticlesHelper;
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.
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderLoader implements AssetLoader {
private static final Logger LOGGER = Logger.getLogger(BlenderLoader.class.getName());
@Override
public Spatial load(AssetInfo assetInfo) throws IOException {
try {
BlenderContext blenderContext = this.setup(assetInfo);
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
animationHelper.loadAnimations();
BlenderKey blenderKey = blenderContext.getBlenderKey();
LoadedFeatures loadedFeatures = new LoadedFeatures();
for (FileBlockHeader block : blenderContext.getBlocks()) {
switch (block.getCode()) {
case BLOCK_OB00:
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Node object = (Node) objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { object.getName(), object.getLocalTranslation().toString(), object.getParent() == null ? "null" : object.getParent().getName() });
}
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;
case BLOCK_MA00:// Material
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext);
loadedFeatures.materials.add(materialContext);
break;
case BLOCK_ME00:// Mesh
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
TemporalMesh temporalMesh = meshHelper.toTemporalMesh(block.getStructure(blenderContext), blenderContext);
loadedFeatures.meshes.add(temporalMesh);
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;
case BLOCK_TE00:
Structure textureStructure = block.getStructure(blenderContext);
int type = ((Number) textureStructure.getFieldValue("type")).intValue();
if (type == TextureHelper.TEX_IMAGE) {
TextureHelper texHelper = blenderContext.getHelper(TextureHelper.class);
Texture texture = texHelper.getTexture(textureStructure, null, blenderContext);
if (texture != null) {// null is returned when texture has no image
loadedFeatures.textures.add(texture);
}
} else {
LOGGER.fine("Only image textures can be loaded as unlinked assets. Generated textures will be applied to an existing object.");
}
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());
}
}
LOGGER.fine("Baking constraints after every feature is loaded.");
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
constraintHelper.bakeConstraints(blenderContext);
LOGGER.fine("Loading scenes and attaching them to the root object.");
for (FileBlockHeader sceneBlock : loadedFeatures.sceneBlocks) {
loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext), 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);
}
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) {
throw new IOException(e.getLocalizedMessage(), e);
} catch (Exception e) {
throw new IOException("Unexpected importer exception occurred: " + e.getLocalizedMessage(), e);
} finally {
this.clear(assetInfo);
}
}
/**
* This method converts the given structure to a scene node.
* @param structure
* structure of a scene
* @param blenderContext the blender context
* @return scene's node
* @throws BlenderFileException
* an exception throw when problems with blender file occur
*/
private Node toScene(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Node result = new Node(structure.getName());
List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase();
for (Structure b : base) {
Pointer pObject = (Pointer) b.getFieldValue("object");
if (pObject.isNotNull()) {
Structure objectStructure = pObject.fetchData().get(0);
Object object = objectHelper.toObject(objectStructure, blenderContext);
if (object instanceof Node) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
}
if (((Node) object).getParent() == null) {
result.attachChild((Spatial) object);
}
if(object instanceof LightNode) {
result.addLight(((LightNode) object).getLight());
}
}
}
}
return result;
}
/**
* This method sets up the loader.
* @param assetInfo
* the asset info
* @throws BlenderFileException
* an exception is throw when something wrong happens with blender file
*/
protected BlenderContext setup(AssetInfo assetInfo) throws BlenderFileException {
// registering loaders
ModelKey modelKey = (ModelKey) assetInfo.getKey();
BlenderKey blenderKey;
if (modelKey instanceof BlenderKey) {
blenderKey = (BlenderKey) modelKey;
} else {
blenderKey = new BlenderKey(modelKey.getName());
}
// opening stream
BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream());
// reading blocks
List<FileBlockHeader> blocks = new ArrayList<FileBlockHeader>();
FileBlockHeader fileBlock;
BlenderContext blenderContext = new BlenderContext();
blenderContext.setBlenderVersion(inputStream.getVersionNumber());
blenderContext.setAssetManager(assetInfo.getManager());
blenderContext.setInputStream(inputStream);
blenderContext.setBlenderKey(blenderKey);
// creating helpers
blenderContext.putHelper(AnimationHelper.class, new AnimationHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(TextureHelper.class, new TextureHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(MeshHelper.class, new MeshHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ObjectHelper.class, new ObjectHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(CurvesHelper.class, new CurvesHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(LightHelper.class, new LightHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(CameraHelper.class, new CameraHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ModifierHelper.class, new ModifierHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(MaterialHelper.class, new MaterialHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext));
blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(inputStream.getVersionNumber(), blenderContext));
// reading the blocks (dna block is automatically saved in the blender context when found)
FileBlockHeader sceneFileBlock = null;
do {
fileBlock = new FileBlockHeader(inputStream, blenderContext);
if (!fileBlock.isDnaBlock()) {
blocks.add(fileBlock);
// save the scene's file block
if (fileBlock.getCode() == BlockCode.BLOCK_SC00) {
sceneFileBlock = fileBlock;
}
}
} while (!fileBlock.isLastBlock());
if (sceneFileBlock != null) {
blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext));
}
// adding locator for linked content
assetInfo.getManager().registerLocator(assetInfo.getKey().getName(), LinkedContentLocator.class);
return blenderContext;
}
/**
* The internal data is only needed during loading so make it unreachable so that the GC can release
* that memory (which can be quite large amount).
*/
protected void clear(AssetInfo assetInfo) {
assetInfo.getManager().unregisterLocator(assetInfo.getKey().getName(), LinkedContentLocator.class);
}
/**
* 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;
}
public static class LinkedContentLocator implements AssetLocator {
private File rootFolder;
@Override
public void setRootPath(String rootPath) {
rootFolder = new File(rootPath);
if(rootFolder.isFile()) {
rootFolder = rootFolder.getParentFile();
}
}
@SuppressWarnings("rawtypes")
@Override
public AssetInfo locate(AssetManager manager, AssetKey key) {
if(key instanceof BlenderKey) {
File linkedAbsoluteFile = new File(key.getName());
if(linkedAbsoluteFile.exists() && linkedAbsoluteFile.isFile()) {
try {
return new StreamAssetInfo(manager, key, new FileInputStream(linkedAbsoluteFile));
} catch (FileNotFoundException e) {
return null;
}
}
File linkedFileInCurrentAssetFolder = new File(rootFolder, linkedAbsoluteFile.getName());
if(linkedFileInCurrentAssetFolder.exists() && linkedFileInCurrentAssetFolder.isFile()) {
try {
return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentAssetFolder));
} catch (FileNotFoundException e) {
return null;
}
}
File linkedFileInCurrentFolder = new File(".", linkedAbsoluteFile.getName());
if(linkedFileInCurrentFolder.exists() && linkedFileInCurrentFolder.isFile()) {
try {
return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentFolder));
} catch (FileNotFoundException e) {
return null;
}
}
}
return null;
}
}
}

@ -0,0 +1,40 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender;
/**
* 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)
*/
public class BlenderModelLoader extends BlenderLoader {
}

@ -0,0 +1,391 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Animation;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl;
import com.jme3.animation.SpatialTrack;
import com.jme3.asset.BlenderKey.AnimationMatchMethod;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.animations.Ipo.ConstIpo;
import com.jme3.scene.plugins.blender.curves.BezierCurve;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
/**
* The helper class that helps in animations loading.
* @author Marcin Roguski (Kaelthas)
*/
public class AnimationHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName());
public AnimationHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* Loads all animations that are stored in the blender file. The animations are not yet applied to the scene features.
* This should be called before objects are loaded.
* @throws BlenderFileException
* an exception is thrown when problems with blender file reading occur
*/
public void loadAnimations() throws BlenderFileException {
LOGGER.info("Loading animations that will be later applied to scene features.");
List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(BlockCode.BLOCK_AC00);
if (actionHeaders != null) {
for (FileBlockHeader header : actionHeaders) {
Structure actionStructure = header.getStructure(blenderContext);
LOGGER.log(Level.INFO, "Found animation: {0}.", actionStructure.getName());
blenderContext.addAction(this.getTracks(actionStructure, blenderContext));
}
}
}
/**
* The method applies animations to the given node. The names of the animations should be the same as actions names in the blender file.
* @param node
* the node to whom the animations will be applied
* @param animationMatchMethod
* the way animation should be matched with node
*/
public void applyAnimations(Node node, AnimationMatchMethod animationMatchMethod) {
List<BlenderAction> actions = this.getActions(node, animationMatchMethod);
if (actions.size() > 0) {
List<Animation> animations = new ArrayList<Animation>();
for (BlenderAction action : actions) {
SpatialTrack[] tracks = action.toTracks(node, blenderContext);
if (tracks != null && tracks.length > 0) {
Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime());
spatialAnimation.setTracks(tracks);
animations.add(spatialAnimation);
blenderContext.addAnimation((Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation);
}
}
if (animations.size() > 0) {
AnimControl control = new AnimControl();
HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size());
for (int i = 0; i < animations.size(); ++i) {
Animation animation = animations.get(i);
anims.put(animation.getName(), animation);
}
control.setAnimations(anims);
node.addControl(control);
}
}
}
/**
* The method applies skeleton animations to the given node.
* @param node
* the node where the animations will be applied
* @param skeleton
* the skeleton of the node
* @param animationMatchMethod
* the way animation should be matched with skeleton
*/
public void applyAnimations(Node node, Skeleton skeleton, AnimationMatchMethod animationMatchMethod) {
node.addControl(new SkeletonControl(skeleton));
blenderContext.setNodeForSkeleton(skeleton, node);
List<BlenderAction> actions = this.getActions(skeleton, animationMatchMethod);
if (actions.size() > 0) {
List<Animation> animations = new ArrayList<Animation>();
for (BlenderAction action : actions) {
BoneTrack[] tracks = action.toTracks(skeleton, blenderContext);
if (tracks != null && tracks.length > 0) {
Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime());
boneAnimation.setTracks(tracks);
animations.add(boneAnimation);
Long animatedNodeOMA = ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue();
blenderContext.addAnimation(animatedNodeOMA, boneAnimation);
}
}
if (animations.size() > 0) {
AnimControl control = new AnimControl(skeleton);
HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size());
for (int i = 0; i < animations.size(); ++i) {
Animation animation = animations.get(i);
anims.put(animation.getName(), animation);
}
control.setAnimations(anims);
node.addControl(control);
// make sure that SkeletonControl is added AFTER the AnimControl
SkeletonControl skeletonControl = node.getControl(SkeletonControl.class);
if (skeletonControl != null) {
node.removeControl(SkeletonControl.class);
node.addControl(skeletonControl);
}
}
}
}
/**
* This method creates an ipo object used for interpolation calculations.
*
* @param ipoStructure
* the structure with ipo definition
* @param blenderContext
* the blender context
* @return the ipo object
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public Ipo fromIpoStructure(Structure ipoStructure, BlenderContext blenderContext) throws BlenderFileException {
Structure curvebase = (Structure) ipoStructure.getFieldValue("curve");
// preparing bezier curves
Ipo result = null;
List<Structure> curves = curvebase.evaluateListBase();// IpoCurve
if (curves.size() > 0) {
BezierCurve[] bezierCurves = new BezierCurve[curves.size()];
int frame = 0;
for (Structure curve : curves) {
Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt");
List<Structure> bezTriples = pBezTriple.fetchData();
int type = ((Number) curve.getFieldValue("adrcode")).intValue();
bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2);
}
curves.clear();
result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
Long ipoOma = ipoStructure.getOldMemoryAddress();
blenderContext.addLoadedFeatures(ipoOma, LoadedDataType.STRUCTURE, ipoStructure);
blenderContext.addLoadedFeatures(ipoOma, LoadedDataType.FEATURE, result);
}
return result;
}
/**
* This method creates an ipo with only a single value. No track type is
* specified so do not use it for calculating tracks.
*
* @param constValue
* the value of this ipo
* @return constant ipo
*/
public Ipo fromValue(float constValue) {
return new ConstIpo(constValue);
}
/**
* This method retuns the bone tracks for animation.
*
* @param actionStructure
* the structure containing the tracks
* @param blenderContext
* the blender context
* @return a list of tracks for the specified animation
* @throws BlenderFileException
* an exception is thrown when there are problems with the blend
* file
*/
private BlenderAction getTracks(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException {
if (blenderVersion < 250) {
return this.getTracks249(actionStructure, blenderContext);
} else {
return this.getTracks250(actionStructure, blenderContext);
}
}
/**
* This method retuns the bone tracks for animation for blender version 2.50
* and higher.
*
* @param actionStructure
* the structure containing the tracks
* @param blenderContext
* the blender context
* @return a list of tracks for the specified animation
* @throws BlenderFileException
* an exception is thrown when there are problems with the blend
* file
*/
private BlenderAction getTracks250(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Getting tracks!");
Structure groups = (Structure) actionStructure.getFieldValue("groups");
List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup
BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps());
int lastFrame = 1;
for (Structure actionGroup : actionGroups) {
String name = actionGroup.getFieldValue("name").toString();
List<Structure> channels = ((Structure) actionGroup.getFieldValue("channels")).evaluateListBase();
BezierCurve[] bezierCurves = new BezierCurve[channels.size()];
int channelCounter = 0;
for (Structure c : channels) {
int type = this.getCurveType(c, blenderContext);
Pointer pBezTriple = (Pointer) c.getFieldValue("bezt");
List<Structure> bezTriples = pBezTriple.fetchData();
bezierCurves[channelCounter++] = new BezierCurve(type, bezTriples, 2);
}
Ipo ipo = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion());
lastFrame = Math.max(lastFrame, ipo.getLastFrame());
blenderAction.featuresTracks.put(name, ipo);
}
blenderAction.stopFrame = lastFrame;
return blenderAction;
}
/**
* This method retuns the bone tracks for animation for blender version 2.49
* (and probably several lower versions too).
*
* @param actionStructure
* the structure containing the tracks
* @param blenderContext
* the blender context
* @return a list of tracks for the specified animation
* @throws BlenderFileException
* an exception is thrown when there are problems with the blend
* file
*/
private BlenderAction getTracks249(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Getting tracks!");
Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase");
List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel
BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps());
int lastFrame = 1;
for (Structure bActionChannel : actionChannels) {
String animatedFeatureName = bActionChannel.getFieldValue("name").toString();
Pointer p = (Pointer) bActionChannel.getFieldValue("ipo");
if (!p.isNull()) {
Structure ipoStructure = p.fetchData().get(0);
Ipo ipo = this.fromIpoStructure(ipoStructure, blenderContext);
if (ipo != null) {// this can happen when ipo with no curves appear in blender file
lastFrame = Math.max(lastFrame, ipo.getLastFrame());
blenderAction.featuresTracks.put(animatedFeatureName, ipo);
}
}
}
blenderAction.stopFrame = lastFrame;
return blenderAction;
}
/**
* This method returns the type of the ipo curve.
*
* @param structure
* the structure must contain the 'rna_path' field and
* 'array_index' field (the type is not important here)
* @param blenderContext
* the blender context
* @return the type of the curve
*/
public int getCurveType(Structure structure, BlenderContext blenderContext) {
// reading rna path first
BlenderInputStream bis = blenderContext.getInputStream();
int currentPosition = bis.getPosition();
Pointer pRnaPath = (Pointer) structure.getFieldValue("rna_path");
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pRnaPath.getOldMemoryAddress());
bis.setPosition(dataFileBlock.getBlockPosition());
String rnaPath = bis.readString();
bis.setPosition(currentPosition);
int arrayIndex = ((Number) structure.getFieldValue("array_index")).intValue();
// determining the curve type
if (rnaPath.endsWith("location")) {
return Ipo.AC_LOC_X + arrayIndex;
}
if (rnaPath.endsWith("rotation_quaternion")) {
return Ipo.AC_QUAT_W + arrayIndex;
}
if (rnaPath.endsWith("scale")) {
return Ipo.AC_SIZE_X + arrayIndex;
}
if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) {
return Ipo.OB_ROT_X + arrayIndex;
}
LOGGER.log(Level.WARNING, "Unknown curve rna path: {0}", rnaPath);
return -1;
}
/**
* The method returns the actions for the given skeleton. The actions represent armature animation in blender.
* @param skeleton
* the skeleton we fetch the actions for
* @param animationMatchMethod
* the method of animation matching
* @return a list of animations for the specified skeleton
*/
private List<BlenderAction> getActions(Skeleton skeleton, AnimationMatchMethod animationMatchMethod) {
List<BlenderAction> result = new ArrayList<BlenderAction>();
// first get a set of bone names
Set<String> boneNames = new HashSet<String>();
for (int i = 0; i < skeleton.getBoneCount(); ++i) {
String boneName = skeleton.getBone(i).getName();
if (boneName != null && boneName.length() > 0) {
boneNames.add(skeleton.getBone(i).getName());
}
}
// finding matches
Set<String> matchingNames = new HashSet<String>();
for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) {
// compute how many action tracks match the skeleton bones' names
for (String boneName : boneNames) {
if (actionEntry.getValue().hasTrackName(boneName)) {
matchingNames.add(boneName);
}
}
BlenderAction action = null;
if (animationMatchMethod == AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH && matchingNames.size() > 0) {
action = actionEntry.getValue();
} else if (matchingNames.size() == actionEntry.getValue().getTracksCount()) {
action = actionEntry.getValue();
}
if (action != null) {
// remove the tracks that do not match the bone names if the matching method is different from ALL_NAMES_MATCH
if (animationMatchMethod != AnimationMatchMethod.ALL_NAMES_MATCH) {
action = action.clone();
action.removeTracksThatAreNotInTheCollection(matchingNames);
}
result.add(action);
}
matchingNames.clear();
}
return result;
}
/**
* The method returns the actions for the given node. The actions represent object animation in blender.
* @param node
* the node we fetch the actions for
* @param animationMatchMethod
* the method of animation matching
* @return a list of animations for the specified node
*/
private List<BlenderAction> getActions(Node node, AnimationMatchMethod animationMatchMethod) {
List<BlenderAction> result = new ArrayList<BlenderAction>();
for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) {
if (actionEntry.getValue().hasTrackName(node.getName())) {
result.add(actionEntry.getValue());
}
}
return result;
}
}

@ -0,0 +1,134 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SpatialTrack;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
/**
* An abstract representation of animation. The data stored here is mainly a
* raw action data loaded from blender. It can later be transformed into
* bone or spatial animation and applied to the specified node.
*
* @author Marcin Roguski (Kaelthas)
*/
public class BlenderAction implements Cloneable {
/** The action name. */
/* package */final String name;
/** Animation speed - frames per second. */
/* package */int fps;
/**
* The last frame of the animation (the last ipo curve node position is
* used as a last frame).
*/
/* package */int stopFrame;
/**
* Tracks of the features. In case of bone animation the keys are the
* names of the bones. In case of spatial animation - the node's name is
* used. A single ipo contains all tracks for location, rotation and
* scales.
*/
/* package */Map<String, Ipo> featuresTracks = new HashMap<String, Ipo>();
public BlenderAction(String name, int fps) {
this.name = name;
this.fps = fps;
}
public void removeTracksThatAreNotInTheCollection(Collection<String> trackNames) {
Map<String, Ipo> newTracks = new HashMap<String, Ipo>();
for (String trackName : trackNames) {
if (featuresTracks.containsKey(trackName)) {
newTracks.put(trackName, featuresTracks.get(trackName));
}
}
featuresTracks = newTracks;
}
@Override
public BlenderAction clone() {
BlenderAction result = new BlenderAction(name, fps);
result.stopFrame = stopFrame;
result.featuresTracks = new HashMap<String, Ipo>(featuresTracks);
return result;
}
/**
* Converts the action into JME spatial animation tracks.
*
* @param node
* the node that will be animated
* @return the spatial tracks for the node
*/
public SpatialTrack[] toTracks(Node node, BlenderContext blenderContext) {
List<SpatialTrack> tracks = new ArrayList<SpatialTrack>(featuresTracks.size());
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) {
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()]);
}
/**
* Converts the action into JME bone animation tracks.
*
* @param skeleton
* the skeleton that will be animated
* @return the bone tracks for the node
*/
public BoneTrack[] toTracks(Skeleton skeleton, BlenderContext blenderContext) {
List<BoneTrack> tracks = new ArrayList<BoneTrack>(featuresTracks.size());
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) {
int boneIndex = skeleton.getBoneIndex(entry.getKey());
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 the name of the action
*/
public String getName() {
return name;
}
/**
* @return the time of animations (in seconds)
*/
public float getAnimationTime() {
return (stopFrame - 1) / (float) fps;
}
/**
* Determines if the current action has a track of a given name.
* CAUTION! The names are case sensitive.
*
* @param name
* the name of the track
* @return <B>true</b> if the track of a given name exists for the
* action and <b>false</b> otherwise
*/
public boolean hasTrackName(String name) {
return featuresTracks.containsKey(name);
}
/**
* @return the amount of tracks in current action
*/
public int getTracksCount() {
return featuresTracks.size();
}
@Override
public String toString() {
return "BlenderTrack [name = " + name + "; tracks = [" + featuresTracks.keySet() + "]]";
}
}

@ -0,0 +1,400 @@
package com.jme3.scene.plugins.blender.animations;
import java.util.ArrayList;
import java.util.List;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
/**
* This class holds the basic data that describes a bone.
*
* @author Marcin Roguski (Kaelthas)
*/
public class BoneContext {
// the flags of the bone
public static final int SELECTED = 0x000001;
public static final int CONNECTED_TO_PARENT = 0x000010;
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).
* So in order to have them loaded properly we need to transform their armature matrix (which blender sees as rotated) to make sure we get identical results.
*/
public static final Matrix4f BONE_ARMATURE_TRANSFORMATION_MATRIX = new Matrix4f(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
private static final int IKFLAG_LOCK_X = 0x01;
private static final int IKFLAG_LOCK_Y = 0x02;
private static final int IKFLAG_LOCK_Z = 0x04;
private static final int IKFLAG_LIMIT_X = 0x08;
private static final int IKFLAG_LIMIT_Y = 0x10;
private static final int IKFLAG_LIMIT_Z = 0x20;
private BlenderContext blenderContext;
/** The OMA of the bone's armature object. */
private Long armatureObjectOMA;
/** The OMA of the model that owns the bone's skeleton. */
private Long skeletonOwnerOma;
/** The structure of the bone. */
private Structure boneStructure;
/** Bone's name. */
private String boneName;
/** The bone's flag. */
private int flag;
/** The bone's matrix in world space. */
private Matrix4f globalBoneMatrix;
/** The bone's matrix in the model space. */
private Matrix4f boneMatrixInModelSpace;
/** The parent context. */
private BoneContext parent;
/** The children of this context. */
private List<BoneContext> children = new ArrayList<BoneContext>();
/** Created bone (available after calling 'buildBone' method). */
private Bone bone;
/** The length of the bone. */
private float length;
/** The bone's deform envelope. */
private BoneEnvelope boneEnvelope;
// The below data is used only for IK constraint computations.
/** The bone's stretch value. */
private float ikStretch;
/** Bone's rotation minimum values. */
private Vector3f limitMin;
/** Bone's rotation maximum values. */
private Vector3f limitMax;
/** The bone's stiffness values (how much it rotates during IK computations. */
private Vector3f stiffness;
/** Values that indicate if any axis' rotation should be limited by some angle. */
private boolean[] limits;
/** Values that indicate if any axis' rotation should be disabled during IK computations. */
private boolean[] locks;
/**
* Constructor. Creates the basic set of bone's data.
*
* @param armatureObjectOMA
* the OMA of the bone's armature object
* @param boneStructure
* the bone's structure
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problem with blender data reading
* occurs
*/
public BoneContext(Long armatureObjectOMA, Structure boneStructure, BlenderContext blenderContext) throws BlenderFileException {
this(boneStructure, armatureObjectOMA, null, blenderContext);
}
/**
* Constructor. Creates the basic set of bone's data.
*
* @param boneStructure
* the bone's structure
* @param armatureObjectOMA
* the OMA of the bone's armature object
* @param parent
* bone's parent (null if the bone is the root bone)
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problem with blender data reading
* occurs
*/
@SuppressWarnings("unchecked")
private BoneContext(Structure boneStructure, Long armatureObjectOMA, BoneContext parent, BlenderContext blenderContext) throws BlenderFileException {
this.parent = parent;
this.blenderContext = blenderContext;
this.boneStructure = boneStructure;
this.armatureObjectOMA = armatureObjectOMA;
boneName = boneStructure.getFieldValue("name").toString();
flag = ((Number) boneStructure.getFieldValue("flag")).intValue();
length = ((Number) boneStructure.getFieldValue("length")).floatValue();
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
// first get the bone matrix in its armature space
globalBoneMatrix = objectHelper.getMatrix(boneStructure, "arm_mat", blenderContext.getBlenderKey().isFixUpAxis());
if (blenderContext.getBlenderKey().isFixUpAxis()) {
// then make sure it is rotated in a proper way to fit the jme bone transformation conventions
globalBoneMatrix.multLocal(BONE_ARMATURE_TRANSFORMATION_MATRIX);
}
Structure armatureStructure = blenderContext.getFileBlock(armatureObjectOMA).getStructure(blenderContext);
Spatial armature = (Spatial) objectHelper.toObject(armatureStructure, blenderContext);
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
Matrix4f armatureWorldMatrix = constraintHelper.toMatrix(armature.getWorldTransform(), new Matrix4f());
// and now compute the final bone matrix in world space
globalBoneMatrix = armatureWorldMatrix.mult(globalBoneMatrix);
// load the bone deformation envelope if necessary
if ((flag & DEFORM) == 0) {// if the flag is NOT set then the DEFORM is in use
boneEnvelope = new BoneEnvelope(boneStructure, armatureWorldMatrix, blenderContext.getBlenderKey().isFixUpAxis());
}
// load bone's pose channel data
Pointer pPose = (Pointer) armatureStructure.getFieldValue("pose");
if (pPose != null && pPose.isNotNull()) {
List<Structure> poseChannels = ((Structure) pPose.fetchData().get(0).getFieldValue("chanbase")).evaluateListBase();
for (Structure poseChannel : poseChannels) {
Long boneOMA = ((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress();
if (boneOMA.equals(this.boneStructure.getOldMemoryAddress())) {
ikStretch = ((Number) poseChannel.getFieldValue("ikstretch")).floatValue();
DynamicArray<Number> limitMin = (DynamicArray<Number>) poseChannel.getFieldValue("limitmin");
this.limitMin = new Vector3f(limitMin.get(0).floatValue(), limitMin.get(1).floatValue(), limitMin.get(2).floatValue());
DynamicArray<Number> limitMax = (DynamicArray<Number>) poseChannel.getFieldValue("limitmax");
this.limitMax = new Vector3f(limitMax.get(0).floatValue(), limitMax.get(1).floatValue(), limitMax.get(2).floatValue());
DynamicArray<Number> stiffness = (DynamicArray<Number>) poseChannel.getFieldValue("stiffness");
this.stiffness = new Vector3f(stiffness.get(0).floatValue(), stiffness.get(1).floatValue(), stiffness.get(2).floatValue());
int ikFlag = ((Number) poseChannel.getFieldValue("ikflag")).intValue();
locks = new boolean[] { (ikFlag & IKFLAG_LOCK_X) != 0, (ikFlag & IKFLAG_LOCK_Y) != 0, (ikFlag & IKFLAG_LOCK_Z) != 0 };
// limits are enabled when locks are disabled, so we ween to take that into account here
limits = new boolean[] { (ikFlag & IKFLAG_LIMIT_X & ~IKFLAG_LOCK_X) != 0, (ikFlag & IKFLAG_LIMIT_Y & ~IKFLAG_LOCK_Y) != 0, (ikFlag & IKFLAG_LIMIT_Z & ~IKFLAG_LOCK_Z) != 0 };
break;// we have found what we need, no need to search further
}
}
}
// create the children
List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase();
for (Structure child : childbase) {
children.add(new BoneContext(child, armatureObjectOMA, this, blenderContext));
}
blenderContext.setBoneContext(boneStructure.getOldMemoryAddress(), this);
}
/**
* This method builds the bone. It recursively builds the bone's children.
*
* @param bones
* a list of bones where the newly created bone will be added
* @param skeletonOwnerOma
* the spatial of the object that will own the skeleton
* @param blenderContext
* the blender context
* @return newly created bone
*/
public Bone buildBone(List<Bone> bones, Long skeletonOwnerOma, BlenderContext blenderContext) {
this.skeletonOwnerOma = skeletonOwnerOma;
Long boneOMA = boneStructure.getOldMemoryAddress();
bone = new Bone(boneName);
bones.add(bone);
blenderContext.addLoadedFeatures(boneOMA, LoadedDataType.STRUCTURE, boneStructure);
blenderContext.addLoadedFeatures(boneOMA, LoadedDataType.FEATURE, bone);
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Structure skeletonOwnerObjectStructure = (Structure) blenderContext.getLoadedFeature(skeletonOwnerOma, LoadedDataType.STRUCTURE);
// I could load 'imat' here, but apparently in some older blenders there were bugs or unfinished functionalities that stored ZERO matrix in imat field
// loading 'obmat' and inverting it makes us avoid errors in such cases
Matrix4f invertedObjectOwnerGlobalMatrix = objectHelper.getMatrix(skeletonOwnerObjectStructure, "obmat", blenderContext.getBlenderKey().isFixUpAxis()).invertLocal();
if (objectHelper.isParent(skeletonOwnerOma, armatureObjectOMA)) {
boneMatrixInModelSpace = globalBoneMatrix.mult(invertedObjectOwnerGlobalMatrix);
} else {
boneMatrixInModelSpace = invertedObjectOwnerGlobalMatrix.mult(globalBoneMatrix);
}
Matrix4f boneLocalMatrix = parent == null ? boneMatrixInModelSpace : parent.boneMatrixInModelSpace.invert().multLocal(boneMatrixInModelSpace);
Vector3f poseLocation = parent == null || !this.is(CONNECTED_TO_PARENT) ? boneLocalMatrix.toTranslationVector() : new Vector3f(0, parent.length, 0);
Quaternion rotation = boneLocalMatrix.toRotationQuat().normalizeLocal();
Vector3f scale = boneLocalMatrix.toScaleVector();
bone.setBindTransforms(poseLocation, rotation, scale);
for (BoneContext child : children) {
bone.addChild(child.buildBone(bones, skeletonOwnerOma, blenderContext));
}
return bone;
}
/**
* @return built bone (available after calling 'buildBone' method)
*/
public Bone getBone() {
return bone;
}
/**
* @return the old memory address of the bone
*/
public Long getBoneOma() {
return boneStructure.getOldMemoryAddress();
}
/**
* The method returns the length of the bone.
* If you want to use it for bone debugger take model space scale into account and do
* something like this:
* <b>boneContext.getLength() * boneContext.getBone().getModelSpaceScale().y</b>.
* Otherwise the bones might not look as they should in the bone debugger.
* @return the length of the bone
*/
public float getLength() {
return length;
}
/**
* @return OMA of the bone's armature object
*/
public Long getArmatureObjectOMA() {
return armatureObjectOMA;
}
/**
* @return the OMA of the model that owns the bone's skeleton
*/
public Long getSkeletonOwnerOma() {
return skeletonOwnerOma;
}
/**
* @return the skeleton the bone of this context belongs to
*/
public Skeleton getSkeleton() {
return blenderContext.getSkeleton(armatureObjectOMA);
}
/**
* @return the initial bone's matrix in model space
*/
public Matrix4f getBoneMatrixInModelSpace() {
return boneMatrixInModelSpace;
}
/**
* @return the vertex assigning envelope of the bone
*/
public BoneEnvelope getBoneEnvelope() {
return boneEnvelope;
}
/**
* @return bone's stretch factor
*/
public float getIkStretch() {
return ikStretch;
}
/**
* @return indicates if the X rotation should be limited
*/
public boolean isLimitX() {
return limits != null ? limits[0] : false;
}
/**
* @return indicates if the Y rotation should be limited
*/
public boolean isLimitY() {
return limits != null ? limits[1] : false;
}
/**
* @return indicates if the Z rotation should be limited
*/
public boolean isLimitZ() {
return limits != null ? limits[2] : false;
}
/**
* @return indicates if the X rotation should be disabled
*/
public boolean isLockX() {
return locks != null ? locks[0] : false;
}
/**
* @return indicates if the Y rotation should be disabled
*/
public boolean isLockY() {
return locks != null ? locks[1] : false;
}
/**
* @return indicates if the Z rotation should be disabled
*/
public boolean isLockZ() {
return locks != null ? locks[2] : false;
}
/**
* @return the minimum values in rotation limitation (if limitation is enabled for specific axis).
*/
public Vector3f getLimitMin() {
return limitMin;
}
/**
* @return the maximum values in rotation limitation (if limitation is enabled for specific axis).
*/
public Vector3f getLimitMax() {
return limitMax;
}
/**
* @return the stiffness of the bone
*/
public Vector3f getStiffness() {
return stiffness;
}
/**
* Tells if the bone is of specified property defined by its flag.
* @param flagMask
* the mask of the flag (constants defined in this class)
* @return <b>true</b> if the bone IS of specified proeprty and <b>false</b> otherwise
*/
public boolean is(int flagMask) {
return (flag & flagMask) != 0;
}
/**
* @return the root bone context of this bone context
*/
public BoneContext getRoot() {
BoneContext result = this;
while (result.parent != null) {
result = result.parent;
}
return result;
}
/**
* @return a number of bones from this bone to its root
*/
public int getDistanceFromRoot() {
int result = 0;
BoneContext boneContext = this;
while (boneContext.parent != null) {
boneContext = boneContext.parent;
++result;
}
return result;
}
@Override
public String toString() {
return "BoneContext: " + boneName;
}
}

@ -0,0 +1,133 @@
package com.jme3.scene.plugins.blender.animations;
import com.jme3.math.Matrix4f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* An implementation of bone envelope. Used when assigning bones to the mesh by envelopes.
*
* @author Marcin Roguski
*/
public class BoneEnvelope {
/** A defined distance that will be included in the envelope space. */
private float distance;
/** The bone's weight. */
private float weight;
/** The radius of the bone's head. */
private float boneHeadRadius;
/** The radius of the bone's tail. */
private float boneTailRadius;
/** Head position in rest pose in world space. */
private Vector3f head;
/** Tail position in rest pose in world space. */
private Vector3f tail;
/**
* The constructor of bone envelope. It reads all the needed data. Take notice that the positions of head and tail
* are computed in the world space and that the points' positions given for computations should be in world space as well.
*
* @param boneStructure
* the blender bone structure
* @param armatureWorldMatrix
* the world matrix of the armature object
* @param fixUpAxis
* a variable that tells if we use the Y-is up axis orientation
*/
@SuppressWarnings("unchecked")
public BoneEnvelope(Structure boneStructure, Matrix4f armatureWorldMatrix, boolean fixUpAxis) {
distance = ((Number) boneStructure.getFieldValue("dist")).floatValue();
weight = ((Number) boneStructure.getFieldValue("weight")).floatValue();
boneHeadRadius = ((Number) boneStructure.getFieldValue("rad_head")).floatValue();
boneTailRadius = ((Number) boneStructure.getFieldValue("rad_tail")).floatValue();
DynamicArray<Number> headArray = (DynamicArray<Number>) boneStructure.getFieldValue("arm_head");
head = new Vector3f(headArray.get(0).floatValue(), headArray.get(1).floatValue(), headArray.get(2).floatValue());
if (fixUpAxis) {
float z = head.z;
head.z = -head.y;
head.y = z;
}
armatureWorldMatrix.mult(head, head);// move the head point to global space
DynamicArray<Number> tailArray = (DynamicArray<Number>) boneStructure.getFieldValue("arm_tail");
tail = new Vector3f(tailArray.get(0).floatValue(), tailArray.get(1).floatValue(), tailArray.get(2).floatValue());
if (fixUpAxis) {
float z = tail.z;
tail.z = -tail.y;
tail.y = z;
}
armatureWorldMatrix.mult(tail, tail);// move the tail point to global space
}
/**
* The method verifies if the given point is inside the envelope.
* @param point
* the point in 3D space (MUST be in a world coordinate space)
* @return <b>true</b> if the point is inside the envelope and <b>false</b> otherwise
*/
public boolean isInEnvelope(Vector3f point) {
Vector3f v = tail.subtract(head);
float boneLength = v.length();
v.normalizeLocal();
// computing a plane that contains 'point' and v is its normal vector
// the plane's equation is: Ax + By + Cz + D = 0, where v = [A, B, C]
float D = -v.dot(point);
// computing a point where a line that contains head and tail crosses the plane
float temp = -(v.dot(head) + D) / v.dot(v);
Vector3f p = head.add(v.x * temp, v.y * temp, v.z * temp);
// determining if the point p is on the same or other side of head than the tail point
Vector3f headToPointOnLineVector = p.subtract(head);
float headToPointLength = headToPointOnLineVector.length();
float cosinus = headToPointOnLineVector.dot(v) / headToPointLength;// the length of v is already = 1; cosinus should be either 1, 0 or -1
if (cosinus < 0 && headToPointLength > boneHeadRadius || headToPointLength > boneLength + boneTailRadius) {
return false;// the point is outside the anvelope
}
// now check if the point is inside and envelope
float pointDistanceFromLine = point.subtract(p).length(), maximumDistance = 0;
if (cosinus < 0) {
// checking if the distance from p to point is inside the half sphere defined by head envelope
// compute the distance from the line to the half sphere border
maximumDistance = boneHeadRadius;
} else if (headToPointLength < boneLength) {
// compute the maximum available distance
if (boneTailRadius > boneHeadRadius) {
// compute the distance from head to p
float headToPDistance = p.subtract(head).length();
// from tangens function we have
float x = headToPDistance * ((boneTailRadius - boneHeadRadius) / boneLength);
maximumDistance = x + boneHeadRadius;
} else if (boneTailRadius < boneHeadRadius) {
// compute the distance from head to p
float tailToPDistance = p.subtract(tail).length();
// from tangens function we have
float x = tailToPDistance * ((boneHeadRadius - boneTailRadius) / boneLength);
maximumDistance = x + boneTailRadius;
} else {
maximumDistance = boneTailRadius;
}
} else {
// checking if the distance from p to point is inside the half sphere defined by tail envelope
maximumDistance = boneTailRadius;
}
return pointDistanceFromLine <= maximumDistance + distance;
}
/**
* @return the weight of the bone
*/
public float getWeight() {
return weight;
}
@Override
public String toString() {
return "BoneEnvelope [d=" + distance + ", w=" + weight + ", hr=" + boneHeadRadius + ", tr=" + boneTailRadius + ", (" + head + ") -> (" + tail + ")]";
}
}

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

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

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

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

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

@ -0,0 +1,397 @@
package com.jme3.scene.plugins.blender.constraints;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Logger;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Animation;
import com.jme3.animation.Bone;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SpatialTrack;
import com.jme3.animation.Track;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.util.TempVars;
/**
* A node that represents either spatial or bone in constraint simulation. The
* node is applied its translation, rotation and scale for each frame of its
* animation. Then the constraints are applied that will eventually alter it.
* After that the feature's transformation is stored in VirtualTrack which is
* converted to new bone or spatial track at the very end.
*
* @author Marcin Roguski (Kaelthas)
*/
public class SimulationNode {
private static final Logger LOGGER = Logger.getLogger(SimulationNode.class.getName());
private Long featureOMA;
/** The blender context. */
private BlenderContext blenderContext;
/** The name of the node (for debugging purposes). */
private String name;
/** A list of children for the node (either bones or child spatials). */
private List<SimulationNode> children = new ArrayList<SimulationNode>();
/** A list of node's animations. */
private List<Animation> animations;
/** The nodes spatial (if null then the boneContext should be set). */
private Spatial spatial;
/** The skeleton of the bone (not null if the node simulated the bone). */
private Skeleton skeleton;
/** Animation controller for the node's feature. */
private AnimControl animControl;
/**
* The star transform of a spatial. Needed to properly reset the spatial to
* its start position.
*/
private Transform spatialStartTransform;
/** Star transformations for bones. Needed to properly reset the bones. */
private Map<Bone, Transform> boneStartTransforms;
/**
* Builds the nodes tree for the given feature. The feature (bone or
* spatial) is found by its OMA. The feature must be a root bone or a root
* spatial.
*
* @param featureOMA
* the OMA of either bone or spatial
* @param blenderContext
* the blender context
*/
public SimulationNode(Long featureOMA, BlenderContext blenderContext) {
this(featureOMA, blenderContext, true);
}
/**
* Creates the node for the feature.
*
* @param featureOMA
* the OMA of either bone or spatial
* @param blenderContext
* the blender context
* @param rootNode
* indicates if the feature is a root bone or root spatial or not
*/
private SimulationNode(Long featureOMA, BlenderContext blenderContext, boolean rootNode) {
this.featureOMA = featureOMA;
this.blenderContext = blenderContext;
Node spatial = (Node) blenderContext.getLoadedFeature(featureOMA, LoadedDataType.FEATURE);
if (blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, spatial) != null) {
skeleton = blenderContext.getSkeleton(featureOMA);
Node nodeWithAnimationControl = blenderContext.getControlledNode(skeleton);
animControl = nodeWithAnimationControl.getControl(AnimControl.class);
boneStartTransforms = new HashMap<Bone, Transform>();
for (int i = 0; i < skeleton.getBoneCount(); ++i) {
Bone bone = skeleton.getBone(i);
boneStartTransforms.put(bone, new Transform(bone.getBindPosition(), bone.getBindRotation(), bone.getBindScale()));
}
} else {
if (rootNode && spatial.getParent() != null) {
throw new IllegalStateException("Given spatial must be a root node!");
}
this.spatial = spatial;
spatialStartTransform = spatial.getLocalTransform().clone();
}
name = '>' + spatial.getName() + '<';
// add children nodes
if (skeleton != null) {
Node node = blenderContext.getControlledNode(skeleton);
Long animatedNodeOMA = ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue();
animations = blenderContext.getAnimations(animatedNodeOMA);
} else {
animations = blenderContext.getAnimations(featureOMA);
for (Spatial child : spatial.getChildren()) {
if (child instanceof Node) {
children.add(new SimulationNode((Long) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, child), blenderContext, false));
}
}
}
}
/**
* Resets the node's feature to its starting transformation.
*/
private void reset() {
if (spatial != null) {
spatial.setLocalTransform(spatialStartTransform);
for (SimulationNode child : children) {
child.reset();
}
} else if (skeleton != null) {
for (Entry<Bone, Transform> entry : boneStartTransforms.entrySet()) {
Transform t = entry.getValue();
entry.getKey().setBindTransforms(t.getTranslation(), t.getRotation(), t.getScale());
entry.getKey().updateModelTransforms();
}
skeleton.reset();
}
}
/**
* Simulates the spatial node.
*/
private void simulateSpatial() {
List<Constraint> constraints = blenderContext.getConstraints(featureOMA);
if (constraints != null && constraints.size() > 0) {
LOGGER.fine("Simulating spatial.");
boolean applyStaticConstraints = true;
if (animations != null) {
for (Animation animation : animations) {
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
int maxFrame = (int) animationTimeBoundaries[0];
float maxTime = animationTimeBoundaries[1];
VirtualTrack vTrack = new VirtualTrack(spatial.getName(), maxFrame, maxTime);
for (Track track : animation.getTracks()) {
for (int frame = 0; frame < maxFrame; ++frame) {
spatial.setLocalTranslation(((SpatialTrack) track).getTranslations()[frame]);
spatial.setLocalRotation(((SpatialTrack) track).getRotations()[frame]);
spatial.setLocalScale(((SpatialTrack) track).getScales()[frame]);
for (Constraint constraint : constraints) {
constraint.apply(frame);
vTrack.setTransform(frame, spatial.getLocalTransform());
}
}
Track newTrack = vTrack.getAsSpatialTrack();
if (newTrack != null) {
animation.removeTrack(track);
animation.addTrack(newTrack);
}
applyStaticConstraints = false;
}
}
}
// if there are no animations then just constraint the static
// object's transformation
if (applyStaticConstraints) {
for (Constraint constraint : constraints) {
constraint.apply(0);
}
}
}
for (SimulationNode child : children) {
child.simulate();
}
}
/**
* Simulates the bone node.
*/
private void simulateSkeleton() {
LOGGER.fine("Simulating skeleton.");
Set<Long> alteredOmas = new HashSet<Long>();
if (animations != null) {
TempVars vars = TempVars.get();
AnimChannel animChannel = animControl.createChannel();
for (Animation animation : animations) {
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation);
int maxFrame = (int) animationTimeBoundaries[0];
float maxTime = animationTimeBoundaries[1];
Map<Integer, VirtualTrack> tracks = new HashMap<Integer, VirtualTrack>();
for (int frame = 0; frame < maxFrame; ++frame) {
// this MUST be done here, otherwise setting next frame of animation will
// lead to possible errors
this.reset();
// first set proper time for all bones in all the tracks ...
for (Track track : animation.getTracks()) {
float time = ((BoneTrack) track).getTimes()[frame];
track.setTime(time, 1, animControl, animChannel, vars);
skeleton.updateWorldVectors();
}
// ... and then apply constraints from the root bone to the last child ...
Set<Long> applied = new HashSet<Long>();
for (Bone rootBone : skeleton.getRoots()) {
// ignore the 0-indexed bone
if (skeleton.getBoneIndex(rootBone) > 0) {
this.applyConstraints(rootBone, alteredOmas, applied, frame, new Stack<Bone>());
}
}
// ... add virtual tracks if necessary, for bones that were altered but had no tracks before ...
for (Long boneOMA : alteredOmas) {
BoneContext boneContext = blenderContext.getBoneContext(boneOMA);
int boneIndex = skeleton.getBoneIndex(boneContext.getBone());
if (!tracks.containsKey(boneIndex)) {
tracks.put(boneIndex, new VirtualTrack(boneContext.getBone().getName(), maxFrame, maxTime));
}
}
alteredOmas.clear();
// ... and fill in another frame in the result track
for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) {
Bone bone = skeleton.getBone(trackEntry.getKey());
Transform startTransform = boneStartTransforms.get(bone);
// track contains differences between the frame position and bind positions of bones/spatials
Vector3f bonePositionDifference = bone.getLocalPosition().subtract(startTransform.getTranslation());
Quaternion boneRotationDifference = startTransform.getRotation().inverse().mult(bone.getLocalRotation()).normalizeLocal();
Vector3f boneScaleDifference = bone.getLocalScale().divide(startTransform.getScale());
trackEntry.getValue().setTransform(frame, new Transform(bonePositionDifference, boneRotationDifference, boneScaleDifference));
}
}
for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) {
Track newTrack = trackEntry.getValue().getAsBoneTrack(trackEntry.getKey());
if (newTrack != null) {
boolean trackReplaced = false;
for (Track track : animation.getTracks()) {
if (((BoneTrack) track).getTargetBoneIndex() == trackEntry.getKey().intValue()) {
animation.removeTrack(track);
animation.addTrack(newTrack);
trackReplaced = true;
break;
}
}
if (!trackReplaced) {
animation.addTrack(newTrack);
}
}
}
}
vars.release();
animControl.clearChannels();
this.reset();
}
}
/**
* Applies constraints to the given bone and its children.
* The goal is to apply constraint from root bone to the last child.
* @param bone
* the bone whose constraints will be applied
* @param alteredOmas
* the set of OMAS of the altered bones (is populated if necessary)
* @param frame
* the current frame of the animation
* @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, Stack<Bone> bonesStack) {
if (!bonesStack.contains(bone)) {
bonesStack.push(bone);
BoneContext boneContext = blenderContext.getBoneContext(bone);
if (!applied.contains(boneContext.getBoneOma())) {
List<Constraint> constraints = this.findConstraints(boneContext.getBoneOma(), blenderContext);
if (constraints != null && constraints.size() > 0) {
for (Constraint constraint : constraints) {
if (constraint.getTargetOMA() != null && constraint.getTargetOMA() > 0L) {
// first apply constraints of the target bone
BoneContext targetBone = blenderContext.getBoneContext(constraint.getTargetOMA());
this.applyConstraints(targetBone.getBone(), alteredOmas, applied, frame, bonesStack);
}
constraint.apply(frame);
if (constraint.getAlteredOmas() != null) {
alteredOmas.addAll(constraint.getAlteredOmas());
}
alteredOmas.add(boneContext.getBoneOma());
}
}
applied.add(boneContext.getBoneOma());
}
List<Bone> children = bone.getChildren();
if (children != null && children.size() > 0) {
for (Bone child : bone.getChildren()) {
this.applyConstraints(child, alteredOmas, applied, frame, bonesStack);
}
}
bonesStack.pop();
}
}
/**
* Simulates the node.
*/
public void simulate() {
this.reset();
if (spatial != null) {
this.simulateSpatial();
} else {
this.simulateSkeleton();
}
}
/**
* Computes the maximum frame and time for the animation. Different tracks
* can have different lengths so here the maximum one is being found.
*
* @param animation
* the animation
* @return maximum frame and time of the animation
*/
private float[] computeAnimationTimeBoundaries(Animation animation) {
int maxFrame = Integer.MIN_VALUE;
float maxTime = -Float.MAX_VALUE;
for (Track track : animation.getTracks()) {
if (track instanceof BoneTrack) {
maxFrame = Math.max(maxFrame, ((BoneTrack) track).getTranslations().length);
maxTime = Math.max(maxTime, ((BoneTrack) track).getTimes()[((BoneTrack) track).getTimes().length - 1]);
} else if (track instanceof SpatialTrack) {
maxFrame = Math.max(maxFrame, ((SpatialTrack) track).getTranslations().length);
maxTime = Math.max(maxTime, ((SpatialTrack) track).getTimes()[((SpatialTrack) track).getTimes().length - 1]);
} else {
throw new IllegalStateException("Unsupported track type for simuation: " + track);
}
}
return new float[] { maxFrame, maxTime };
}
/**
* Finds constraints for the node's features.
*
* @param ownerOMA
* the feature's OMA
* @param blenderContext
* the blender context
* @return a list of feature's constraints or empty list if none were found
*/
private List<Constraint> findConstraints(Long ownerOMA, BlenderContext blenderContext) {
List<Constraint> result = new ArrayList<Constraint>();
List<Constraint> constraints = blenderContext.getConstraints(ownerOMA);
if (constraints != null) {
for (Constraint constraint : constraints) {
if (constraint.isImplemented() && constraint.validate() && constraint.isTrackToBeChanged()) {
result.add(constraint);
}
// TODO: add proper warnings to some map or set so that they are not logged on every frame
}
}
return result.size() > 0 ? result : null;
}
@Override
public String toString() {
return name;
}
}

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

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

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

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

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

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

@ -0,0 +1,236 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import org.ejml.simple.SimpleMatrix;
import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.math.DQuaternion;
import com.jme3.scene.plugins.blender.math.DTransform;
import com.jme3.scene.plugins.blender.math.Matrix;
import com.jme3.scene.plugins.blender.math.Vector3d;
/**
* A definiotion of a Inverse Kinematics constraint. This implementation uses Jacobian pseudoinverse algorithm.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ConstraintDefinitionIK extends ConstraintDefinition {
private static final float MIN_DISTANCE = 0.001f;
private static final float MIN_ANGLE_CHANGE = 0.001f;
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. */
private int bonesAffected;
/** Indicates if the tail of the bone should be used or not. */
private boolean useTail;
/** The amount of iterations of the algorithm. */
private int iterations;
/** The count of bones' chain. */
private int bonesCount = -1;
public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
bonesAffected = ((Number) constraintData.getFieldValue("rootbone")).intValue();
iterations = ((Number) constraintData.getFieldValue("iterations")).intValue();
useTail = (flag & FLAG_USE_TAIL) != 0;
if ((flag & FLAG_POSITION) == 0) {
trackToBeChanged = false;
}
if (trackToBeChanged) {
alteredOmas = new HashSet<Long>();
}
}
/**
* 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
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (influence == 0 || !trackToBeChanged || targetTransform == null || bonesCount == 0) {
return;// no need to do anything
}
if (bones == null) {
bones = new BonesChain((Bone) this.getOwner(), useTail, bonesAffected, alteredOmas, blenderContext);
}
if (bones.size() == 0) {
bonesCount = 0;
return;// no need to do anything
}
double distanceFromTarget = Double.MAX_VALUE;
target.set(targetTransform.getTranslation().x, targetTransform.getTranslation().y, targetTransform.getTranslation().z);
if (bonesCount < 0) {
bonesCount = bones.size();
rotationVectors = new Vector3d[bonesCount];
for (int i = 0; i < bonesCount; ++i) {
rotationVectors[i] = new Vector3d();
}
J = new Matrix(3, bonesCount);
}
BoneContext topBone = bones.get(0);
for (int i = 0; i < iterations; ++i) {
DTransform topBoneTransform = bones.getWorldTransform(topBone);
Vector3d e = topBoneTransform.getTranslation().add(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
distanceFromTarget = e.distance(target);
if (distanceFromTarget <= MIN_DISTANCE) {
break;
}
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 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();
SimpleMatrix deltaThetas = J_1.mult(deltaP);
if (deltaThetas.elementMaxAbs() < MIN_ANGLE_CHANGE) {
break;
}
for (int j = 0; j < deltaThetas.numRows(); ++j) {
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()) {
tempDQuaternion.set(0, tempDQuaternion.getY(), tempDQuaternion.getZ(), tempDQuaternion.getW());
}
if (boneContext.isLockY()) {
tempDQuaternion.set(tempDQuaternion.getX(), 0, tempDQuaternion.getZ(), tempDQuaternion.getW());
}
if (boneContext.isLockZ()) {
tempDQuaternion.set(tempDQuaternion.getX(), tempDQuaternion.getY(), 0, tempDQuaternion.getW());
}
}
DTransform boneTransform = bones.getWorldTransform(boneContext);
boneTransform.getRotation().set(tempDQuaternion.mult(boneTransform.getRotation()));
bones.setWorldTransform(boneContext, boneTransform);
}
}
// applying the results
for (int i = bonesCount - 1; i >= 0; --i) {
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
public String getConstraintTypeName() {
return "Inverse kinematics";
}
@Override
public boolean isTargetRequired() {
return true;
}
/**
* 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 static class BonesChain extends ArrayList<BoneContext> {
private static final long serialVersionUID = -1850524345643600718L;
private List<Matrix> localBonesMatrices = new ArrayList<Matrix>();
public BonesChain(Bone bone, boolean useTail, int bonesAffected, Collection<Long> alteredOmas, BlenderContext blenderContext) {
if (bone != null) {
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
if (!useTail) {
bone = bone.getParent();
}
while (bone != null && (bonesAffected <= 0 || this.size() < bonesAffected)) {
BoneContext boneContext = blenderContext.getBoneContext(bone);
this.add(boneContext);
alteredOmas.add(boneContext.getBoneOma());
Transform transform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD);
localBonesMatrices.add(new DTransform(transform).toMatrix());
bone = bone.getParent();
}
if(localBonesMatrices.size() > 0) {
// making the matrices describe the local transformation
Matrix parentWorldMatrix = localBonesMatrices.get(localBonesMatrices.size() - 1);
for(int i=localBonesMatrices.size() - 2;i>=0;--i) {
SimpleMatrix m = parentWorldMatrix.invert().mult(localBonesMatrices.get(i));
parentWorldMatrix = localBonesMatrices.get(i);
localBonesMatrices.set(i, new Matrix(m));
}
}
}
}
public DTransform getWorldTransform(BoneContext bone) {
int index = this.indexOf(bone);
return this.getWorldMatrix(index).toTransform();
}
public void setWorldTransform(BoneContext bone, DTransform transform) {
int index = this.indexOf(bone);
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);
}
localBonesMatrices.set(index, boneMatrix);
}
public Matrix getWorldMatrix(int index) {
if (index == this.size() - 1) {
return new Matrix(localBonesMatrices.get(this.size() - 1));
}
SimpleMatrix result = this.getWorldMatrix(index + 1);
result = result.mult(localBonesMatrices.get(index));
return new Matrix(result);
}
}
}

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

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

@ -0,0 +1,61 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.animation.Bone;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* This class represents 'Maintain volume' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ConstraintDefinitionMaintainVolume extends ConstraintDefinition {
private static final int FLAG_MASK_X = 0;
private static final int FLAG_MASK_Y = 1;
private static final int FLAG_MASK_Z = 2;
private float volume;
public ConstraintDefinitionMaintainVolume(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
volume = (float) Math.sqrt(((Number) constraintData.getFieldValue("volume")).floatValue());
trackToBeChanged = volume != 1 && (flag & (FLAG_MASK_X | FLAG_MASK_Y | FLAG_MASK_Z)) != 0;
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (trackToBeChanged && influence > 0) {
// the maintain volume constraint is applied directly to object's scale, so no need to do it again
// but in case of bones we need to make computations
if (this.getOwner() instanceof Bone) {
Transform ownerTransform = this.getOwnerTransform(ownerSpace);
switch (flag) {
case FLAG_MASK_X:
ownerTransform.getScale().multLocal(1, volume, volume);
break;
case FLAG_MASK_Y:
ownerTransform.getScale().multLocal(volume, 1, volume);
break;
case FLAG_MASK_Z:
ownerTransform.getScale().multLocal(volume, volume, 1);
break;
default:
throw new IllegalStateException("Unknown flag value: " + flag);
}
this.applyOwnerTransform(ownerTransform, ownerSpace);
}
}
}
@Override
public String getConstraintTypeName() {
return "Maintain volume";
}
@Override
public boolean isTargetRequired() {
return false;
}
}

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

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

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

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

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

@ -0,0 +1,81 @@
package com.jme3.scene.plugins.blender.constraints.definitions;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.math.Matrix4f;
import com.jme3.math.Transform;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.util.TempVars;
/**
* This class represents 'Trans like' constraint type in blender.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ConstraintDefinitionTransLike extends ConstraintDefinition {
private Long targetOMA;
private String subtargetName;
public ConstraintDefinitionTransLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) {
super(constraintData, ownerOMA, blenderContext);
Pointer pTarget = (Pointer) constraintData.getFieldValue("tar");
targetOMA = pTarget.getOldMemoryAddress();
Object subtarget = constraintData.getFieldValue("subtarget");
if (subtarget != null) {
subtargetName = subtarget.toString();
}
}
@Override
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) {
if (influence == 0 || targetTransform == null) {
return;// no need to do anything
}
Object target = this.getTarget();// Bone or Node
Object owner = this.getOwner();// Bone or Node
if (!target.getClass().equals(owner.getClass())) {
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
TempVars tempVars = TempVars.get();
Matrix4f m = constraintHelper.toMatrix(targetTransform, tempVars.tempMat4);
tempVars.tempMat42.set(BoneContext.BONE_ARMATURE_TRANSFORMATION_MATRIX);
if (target instanceof Bone) {
tempVars.tempMat42.invertLocal();
}
m = m.multLocal(tempVars.tempMat42);
tempVars.release();
targetTransform = new Transform(m.toTranslationVector(), m.toRotationQuat(), m.toScaleVector());
}
this.applyOwnerTransform(targetTransform, ownerSpace);
}
/**
* @return the target feature; it is either Node or Bone (vertex group subtarger is not yet supported)
*/
private Object getTarget() {
Object target = blenderContext.getLoadedFeature(targetOMA, LoadedDataType.FEATURE);
if (subtargetName != null && blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, target) != null) {
Skeleton skeleton = blenderContext.getSkeleton(targetOMA);
target = skeleton.getBone(subtargetName);
}
return target;
}
@Override
public String getConstraintTypeName() {
return "Copy transforms";
}
@Override
public boolean isTargetRequired() {
return true;
}
}

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

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

@ -0,0 +1,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.scene.plugins.blender.curves;
import java.util.logging.Logger;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A class that is used in mesh calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class CurvesHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(CurvesHelper.class.getName());
/** Minimum basis U function degree for NURBS curves and surfaces. */
protected int minimumBasisUFunctionDegree = 4;
/** Minimum basis V function degree for NURBS curves and surfaces. */
protected int minimumBasisVFunctionDegree = 4;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in
* different blender versions.
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public CurvesHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
public CurvesTemporalMesh toCurve(Structure curveStructure, BlenderContext blenderContext) throws BlenderFileException {
CurvesTemporalMesh result = new CurvesTemporalMesh(curveStructure, blenderContext);
if (blenderContext.getBlenderKey().isLoadObjectProperties()) {
LOGGER.fine("Reading custom properties.");
result.setProperties(this.loadProperties(curveStructure, blenderContext));
}
return result;
}
/**
* The method transforms the bevel along the curve.
*
* @param bevel
* the bevel to be transformed
* @param prevPos
* previous curve point
* @param currPos
* current curve point (here the center of the new bevel will be
* set)
* @param nextPos
* next curve point
* @return points of transformed bevel
*/
protected Vector3f[] transformBevel(Vector3f[] bevel, Vector3f prevPos, Vector3f currPos, Vector3f nextPos) {
bevel = bevel.clone();
// currPos and directionVector define the line in 3D space
Vector3f directionVector = prevPos != null ? currPos.subtract(prevPos) : nextPos.subtract(currPos);
directionVector.normalizeLocal();
// plane is described by equation: Ax + By + Cz + D = 0 where planeNormal = [A, B, C] and D = -(Ax + By + Cz)
Vector3f planeNormal = null;
if (prevPos != null) {
planeNormal = currPos.subtract(prevPos).normalizeLocal();
if (nextPos != null) {
planeNormal.addLocal(nextPos.subtract(currPos).normalizeLocal()).normalizeLocal();
}
} else {
planeNormal = nextPos.subtract(currPos).normalizeLocal();
}
float D = -planeNormal.dot(currPos);// D = -(Ax + By + Cz)
// now we need to compute paralell cast of each bevel point on the plane, the leading line is already known
// parametric equation of a line: x = px + vx * t; y = py + vy * t; z = pz + vz * t
// where p = currPos and v = directionVector
// using x, y and z in plane equation we get value of 't' that will allow us to compute the point where plane and line cross
float temp = planeNormal.dot(directionVector);
for (int i = 0; i < bevel.length; ++i) {
float t = -(planeNormal.dot(bevel[i]) + D) / temp;
if (fixUpAxis) {
bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, bevel[i].y + directionVector.y * t, bevel[i].z + directionVector.z * t);
} else {
bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, -bevel[i].z + directionVector.z * t, bevel[i].y + directionVector.y * t);
}
}
return bevel;
}
/**
* This method transforms the first line of the bevel points positioning it
* on the first point of the curve.
*
* @param startingLinePoints
* the vbevel shape points
* @param firstCurvePoint
* the first curve's point
* @param secondCurvePoint
* the second curve's point
* @return points of transformed bevel
*/
protected Vector3f[] transformToFirstLineOfBevelPoints(Vector3f[] startingLinePoints, Vector3f firstCurvePoint, Vector3f secondCurvePoint) {
Vector3f planeNormal = secondCurvePoint.subtract(firstCurvePoint).normalizeLocal();
float angle = FastMath.acos(planeNormal.dot(Vector3f.UNIT_X));
Vector3f rotationVector = Vector3f.UNIT_X.cross(planeNormal).normalizeLocal();
Matrix4f m = new Matrix4f();
m.setRotationQuaternion(new Quaternion().fromAngleAxis(angle, rotationVector));
m.setTranslation(firstCurvePoint);
Vector3f temp = new Vector3f();
Vector3f[] verts = new Vector3f[startingLinePoints.length];
for (int i = 0; i < verts.length; ++i) {
verts[i] = m.mult(startingLinePoints[i], temp).clone();
}
return verts;
}
}

@ -0,0 +1,890 @@
package com.jme3.scene.plugins.blender.curves;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Logger;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.FastMath;
import com.jme3.math.Spline;
import com.jme3.math.Spline.SplineType;
import com.jme3.math.Vector3f;
import com.jme3.math.Vector4f;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.meshes.Edge;
import com.jme3.scene.plugins.blender.meshes.Face;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.shape.Curve;
import com.jme3.scene.shape.Surface;
import com.jme3.util.BufferUtils;
/**
* A temporal mesh for curves and surfaces. It works in similar way as TemporalMesh for meshes.
* It prepares all necessary lines and faces and allows to apply modifiers just like in regular temporal mesh.
*
* @author Marcin Roguski (Kaelthas)
*/
public class CurvesTemporalMesh extends TemporalMesh {
private static final Logger LOGGER = Logger.getLogger(CurvesTemporalMesh.class.getName());
private static final int TYPE_BEZIER = 0x0001;
private static final int TYPE_NURBS = 0x0004;
private static final int FLAG_3D = 0x0001;
private static final int FLAG_FRONT = 0x0002;
private static final int FLAG_BACK = 0x0004;
private static final int FLAG_FILL_CAPS = 0x4000;
private static final int FLAG_SMOOTH = 0x0001;
protected CurvesHelper curvesHelper;
protected boolean is2D;
protected boolean isFront;
protected boolean isBack;
protected boolean fillCaps;
protected float bevelStart;
protected float bevelEnd;
protected List<BezierLine> beziers = new ArrayList<BezierLine>();
protected CurvesTemporalMesh bevelObject;
protected CurvesTemporalMesh taperObject;
/** The scale that is used if the curve is a bevel or taper curve. */
protected Vector3f scale = new Vector3f(1, 1, 1);
/**
* The constructor creates an empty temporal mesh.
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this will never be thrown here
*/
protected CurvesTemporalMesh(BlenderContext blenderContext) throws BlenderFileException {
super(null, blenderContext, false);
}
/**
* Loads the temporal mesh from the given curve structure. The mesh can be either curve or surface.
* @param curveStructure
* the structure that contains the curve/surface data
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problems with reading occur
*/
public CurvesTemporalMesh(Structure curveStructure, BlenderContext blenderContext) throws BlenderFileException {
this(curveStructure, new Vector3f(1, 1, 1), true, blenderContext);
}
/**
* Loads the temporal mesh from the given curve structure. The mesh can be either curve or surface.
* @param curveStructure
* the structure that contains the curve/surface data
* @param scale
* the scale used if the current curve is used as a bevel curve
* @param loadBevelAndTaper indicates if bevel and taper should be loaded (this is not needed for curves that are loaded to be used as bevel and taper)
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problems with reading occur
*/
@SuppressWarnings("unchecked")
private CurvesTemporalMesh(Structure curveStructure, Vector3f scale, boolean loadBevelAndTaper, BlenderContext blenderContext) throws BlenderFileException {
super(curveStructure, blenderContext, false);
name = curveStructure.getName();
curvesHelper = blenderContext.getHelper(CurvesHelper.class);
this.scale = scale;
int flag = ((Number) curveStructure.getFieldValue("flag")).intValue();
is2D = (flag & FLAG_3D) == 0;
if (is2D) {
// TODO: add support for 3D flag
LOGGER.warning("2D flag not yet supported for curves!");
}
isFront = (flag & FLAG_FRONT) != 0;
isBack = (flag & FLAG_BACK) != 0;
fillCaps = (flag & FLAG_FILL_CAPS) != 0;
bevelStart = ((Number) curveStructure.getFieldValue("bevfac1", 0)).floatValue();
bevelEnd = ((Number) curveStructure.getFieldValue("bevfac2", 1)).floatValue();
if (bevelStart > bevelEnd) {
float temp = bevelStart;
bevelStart = bevelEnd;
bevelEnd = temp;
}
LOGGER.fine("Reading nurbs (and sorting them by material).");
Map<Number, List<Structure>> nurbs = new HashMap<Number, List<Structure>>();
List<Structure> nurbStructures = ((Structure) curveStructure.getFieldValue("nurb")).evaluateListBase();
for (Structure nurb : nurbStructures) {
Number matNumber = (Number) nurb.getFieldValue("mat_nr");
List<Structure> nurbList = nurbs.get(matNumber);
if (nurbList == null) {
nurbList = new ArrayList<Structure>();
nurbs.put(matNumber, nurbList);
}
nurbList.add(nurb);
}
LOGGER.fine("Getting materials.");
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
materials = materialHelper.getMaterials(curveStructure, blenderContext);
if (materials != null) {
for (MaterialContext materialContext : materials) {
materialContext.setFaceCullMode(FaceCullMode.Off);
}
}
LOGGER.fine("Getting or creating bevel object.");
bevelObject = loadBevelAndTaper ? this.loadBevelObject(curveStructure) : null;
LOGGER.fine("Getting taper object.");
Pointer pTaperObject = (Pointer) curveStructure.getFieldValue("taperobj");
if (bevelObject != null && pTaperObject.isNotNull()) {
Structure taperObjectStructure = pTaperObject.fetchData().get(0);
DynamicArray<Number> scaleArray = (DynamicArray<Number>) taperObjectStructure.getFieldValue("size");
scale = blenderContext.getBlenderKey().isFixUpAxis() ? new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue()) : new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(2).floatValue(), scaleArray.get(1).floatValue());
Pointer pTaperStructure = (Pointer) taperObjectStructure.getFieldValue("data");
Structure taperStructure = pTaperStructure.fetchData().get(0);
taperObject = new CurvesTemporalMesh(taperStructure, blenderContext);
}
LOGGER.fine("Creating the result curves.");
for (Entry<Number, List<Structure>> nurbEntry : nurbs.entrySet()) {
for (Structure nurb : nurbEntry.getValue()) {
int type = ((Number) nurb.getFieldValue("type")).intValue();
if ((type & TYPE_BEZIER) != 0) {
this.loadBezierCurve(nurb, nurbEntry.getKey().intValue());
} else if ((type & TYPE_NURBS) != 0) {
this.loadNurbSurface(nurb, nurbEntry.getKey().intValue());
} else {
throw new BlenderFileException("Unknown curve type: " + type);
}
}
}
if (bevelObject != null && beziers.size() > 0) {
this.append(this.applyBevelAndTaper(this, bevelObject, taperObject, blenderContext));
} else {
for (BezierLine bezierLine : beziers) {
int originalVerticesAmount = vertices.size();
vertices.add(bezierLine.vertices[0]);
Vector3f v = bezierLine.vertices[1].subtract(bezierLine.vertices[0]).normalizeLocal();
float temp = v.x;
v.x = -v.y;
v.y = temp;
v.z = 0;
normals.add(v);// this will be smoothed in the next iteration
for (int i = 1; i < bezierLine.vertices.length; ++i) {
vertices.add(bezierLine.vertices[i]);
edges.add(new Edge(originalVerticesAmount + i - 1, originalVerticesAmount + i, 0, false, this));
// generating normal for vertex at 'i'
v = bezierLine.vertices[i].subtract(bezierLine.vertices[i - 1]).normalizeLocal();
temp = v.x;
v.x = -v.y;
v.y = temp;
v.z = 0;
// make the previous normal smooth
normals.get(i - 1).addLocal(v).multLocal(0.5f).normalizeLocal();
normals.add(v);// this will be smoothed in the next iteration
}
}
}
}
/**
* The method computes the value of a point at the certain relational distance from its beginning.
* @param alongRatio
* the relative distance along the curve; should be a value between 0 and 1 inclusive;
* if the value exceeds the boundaries it is truncated to them
* @return computed value along the curve
*/
private Vector3f getValueAlongCurve(float alongRatio) {
alongRatio = FastMath.clamp(alongRatio, 0, 1);
Vector3f result = new Vector3f();
float probeLength = this.getLength() * alongRatio, length = 0;
for (BezierLine bezier : beziers) {
float edgeLength = bezier.getLength();
if (length + edgeLength >= probeLength) {
float ratioAlongEdge = (probeLength - length) / edgeLength;
return bezier.getValueAlongCurve(ratioAlongEdge);
}
length += edgeLength;
}
return result;
}
/**
* @return the length of the curve
*/
private float getLength() {
float result = 0;
for (BezierLine bezier : beziers) {
result += bezier.getLength();
}
return result;
}
/**
* The methods loads the bezier curve from the given structure.
* @param nurbStructure
* the structure containing a single curve definition
* @param materialIndex
* the index of this segment's material
* @throws BlenderFileException
* an exception is thrown when problems with reading occur
*/
private void loadBezierCurve(Structure nurbStructure, int materialIndex) throws BlenderFileException {
Pointer pBezierTriple = (Pointer) nurbStructure.getFieldValue("bezt");
if (pBezierTriple.isNotNull()) {
int resolution = ((Number) nurbStructure.getFieldValue("resolu")).intValue();
boolean cyclic = (((Number) nurbStructure.getFieldValue("flagu")).intValue() & 0x01) != 0;
boolean smooth = (((Number) nurbStructure.getFieldValue("flag")).intValue() & FLAG_SMOOTH) != 0;
// creating the curve object
BezierCurve bezierCurve = new BezierCurve(0, pBezierTriple.fetchData(), 3, blenderContext.getBlenderKey().isFixUpAxis());
List<Vector3f> controlPoints = bezierCurve.getControlPoints();
if (cyclic) {
// copy the first three points at the end
for (int i = 0; i < 3; ++i) {
controlPoints.add(controlPoints.get(i));
}
}
// removing the first and last handles
controlPoints.remove(0);
controlPoints.remove(controlPoints.size() - 1);
// creating curve
Curve curve = new Curve(new Spline(SplineType.Bezier, controlPoints, 0, false), resolution);
FloatBuffer vertsBuffer = (FloatBuffer) curve.getBuffer(Type.Position).getData();
beziers.add(new BezierLine(BufferUtils.getVector3Array(vertsBuffer), materialIndex, smooth, cyclic));
}
}
/**
* This method loads the NURBS curve or surface.
* @param nurb
* the NURBS data structure
* @throws BlenderFileException
* an exception is thrown when problems with reading occur
*/
@SuppressWarnings("unchecked")
private void loadNurbSurface(Structure nurb, int materialIndex) throws BlenderFileException {
// loading the knots
List<Float>[] knots = new List[2];
Pointer[] pKnots = new Pointer[] { (Pointer) nurb.getFieldValue("knotsu"), (Pointer) nurb.getFieldValue("knotsv") };
for (int i = 0; i < knots.length; ++i) {
if (pKnots[i].isNotNull()) {
FileBlockHeader fileBlockHeader = blenderContext.getFileBlock(pKnots[i].getOldMemoryAddress());
BlenderInputStream blenderInputStream = blenderContext.getInputStream();
blenderInputStream.setPosition(fileBlockHeader.getBlockPosition());
int knotsAmount = fileBlockHeader.getCount() * fileBlockHeader.getSize() / 4;
knots[i] = new ArrayList<Float>(knotsAmount);
for (int j = 0; j < knotsAmount; ++j) {
knots[i].add(Float.valueOf(blenderInputStream.readFloat()));
}
}
}
// loading the flags and orders (basis functions degrees)
int flag = ((Number) nurb.getFieldValue("flag")).intValue();
boolean smooth = (flag & FLAG_SMOOTH) != 0;
int flagU = ((Number) nurb.getFieldValue("flagu")).intValue();
int flagV = ((Number) nurb.getFieldValue("flagv")).intValue();
int orderU = ((Number) nurb.getFieldValue("orderu")).intValue();
int orderV = ((Number) nurb.getFieldValue("orderv")).intValue();
// loading control points and their weights
int pntsU = ((Number) nurb.getFieldValue("pntsu")).intValue();
int pntsV = ((Number) nurb.getFieldValue("pntsv")).intValue();
List<Structure> bPoints = ((Pointer) nurb.getFieldValue("bp")).fetchData();
List<List<Vector4f>> controlPoints = new ArrayList<List<Vector4f>>(pntsV);
for (int i = 0; i < pntsV; ++i) {
List<Vector4f> uControlPoints = new ArrayList<Vector4f>(pntsU);
for (int j = 0; j < pntsU; ++j) {
DynamicArray<Float> vec = (DynamicArray<Float>) bPoints.get(j + i * pntsU).getFieldValue("vec");
if (blenderContext.getBlenderKey().isFixUpAxis()) {
uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(2).floatValue(), -vec.get(1).floatValue(), vec.get(3).floatValue()));
} else {
uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(1).floatValue(), vec.get(2).floatValue(), vec.get(3).floatValue()));
}
}
if ((flagU & 0x01) != 0) {
for (int k = 0; k < orderU - 1; ++k) {
uControlPoints.add(uControlPoints.get(k));
}
}
controlPoints.add(uControlPoints);
}
if ((flagV & 0x01) != 0) {
for (int k = 0; k < orderV - 1; ++k) {
controlPoints.add(controlPoints.get(k));
}
}
int originalVerticesAmount = vertices.size();
int resolu = ((Number) nurb.getFieldValue("resolu")).intValue();
if (knots[1] == null) {// creating the NURB curve
Curve curve = new Curve(new Spline(controlPoints.get(0), knots[0]), resolu);
FloatBuffer vertsBuffer = (FloatBuffer) curve.getBuffer(Type.Position).getData();
beziers.add(new BezierLine(BufferUtils.getVector3Array(vertsBuffer), materialIndex, smooth, false));
} else {// creating the NURB surface
int resolv = ((Number) nurb.getFieldValue("resolv")).intValue();
int uSegments = resolu * controlPoints.get(0).size() - 1;
int vSegments = resolv * controlPoints.size() - 1;
Surface nurbSurface = Surface.createNurbsSurface(controlPoints, knots, uSegments, vSegments, orderU, orderV, smooth);
FloatBuffer vertsBuffer = (FloatBuffer) nurbSurface.getBuffer(Type.Position).getData();
vertices.addAll(Arrays.asList(BufferUtils.getVector3Array(vertsBuffer)));
FloatBuffer normalsBuffer = (FloatBuffer) nurbSurface.getBuffer(Type.Normal).getData();
normals.addAll(Arrays.asList(BufferUtils.getVector3Array(normalsBuffer)));
IndexBuffer indexBuffer = nurbSurface.getIndexBuffer();
for (int i = 0; i < indexBuffer.size(); i += 3) {
int index1 = indexBuffer.get(i) + originalVerticesAmount;
int index2 = indexBuffer.get(i + 1) + originalVerticesAmount;
int index3 = indexBuffer.get(i + 2) + originalVerticesAmount;
faces.add(new Face(new Integer[] { index1, index2, index3 }, smooth, materialIndex, null, null, this));
}
}
}
/**
* The method loads the bevel object that should be applied to curve. It can either be another curve or a generated one
* based on the bevel generating parameters in blender.
* @param curveStructure
* the structure with the curve's data (the curve being loaded, NOT the bevel curve)
* @return the curve's bevel object
* @throws BlenderFileException
* an exception is thrown when problems with reading occur
*/
@SuppressWarnings("unchecked")
private CurvesTemporalMesh loadBevelObject(Structure curveStructure) throws BlenderFileException {
CurvesTemporalMesh bevelObject = null;
Pointer pBevelObject = (Pointer) curveStructure.getFieldValue("bevobj");
boolean cyclic = false;
if (pBevelObject.isNotNull()) {
Structure bevelObjectStructure = pBevelObject.fetchData().get(0);
DynamicArray<Number> scaleArray = (DynamicArray<Number>) bevelObjectStructure.getFieldValue("size");
Vector3f scale = blenderContext.getBlenderKey().isFixUpAxis() ? new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue()) : new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(2).floatValue(), scaleArray.get(1).floatValue());
Pointer pBevelStructure = (Pointer) bevelObjectStructure.getFieldValue("data");
Structure bevelStructure = pBevelStructure.fetchData().get(0);
bevelObject = new CurvesTemporalMesh(bevelStructure, scale, false, blenderContext);
// transforming the bezier lines from plane XZ to plane YZ
for (BezierLine bl : bevelObject.beziers) {
for (Vector3f v : bl.vertices) {
// casting the bezier curve orthogonally on the plane XZ (making Y = 0) and then moving the plane XZ to ZY in a way that:
// -Z => +Y and +X => +Z and +Y => +X (but because casting would make Y = 0, then we simply set X = 0)
v.y = -v.z;
v.z = v.x;
v.x = 0;
}
// bevel curves should not have repeated the first vertex at the end when they are cyclic (this is handled differently)
if (bl.isCyclic()) {
bl.removeLastVertex();
}
}
} else {
fillCaps = false;// this option is inactive in blender when there is no bevel object applied
int bevResol = ((Number) curveStructure.getFieldValue("bevresol")).intValue();
float extrude = ((Number) curveStructure.getFieldValue("ext1")).floatValue();
float bevelDepth = ((Number) curveStructure.getFieldValue("ext2")).floatValue();
float offset = ((Number) curveStructure.getFieldValue("offset", 0)).floatValue();
if (offset != 0) {
// TODO: add support for offset parameter
LOGGER.warning("Offset parameter not yet supported.");
}
Curve bevelCurve = null;
if (bevelDepth > 0.0f) {
float handlerLength = bevelDepth / 2.0f;
cyclic = !isFront && !isBack;
List<Vector3f> conrtolPoints = new ArrayList<Vector3f>();
// blenders from 2.49 to 2.52 did not pay attention to fron and back faces
// so in order to draw the scene exactly as it is in different blender versions the blender version is checked here
// when neither fron and back face is selected all version behave the same and draw full bevel around the curve
if (cyclic || blenderContext.getBlenderVersion() < 253) {
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, 0));
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, -extrude - handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, -extrude, -bevelDepth));
conrtolPoints.add(new Vector3f(0, -extrude + handlerLength, -bevelDepth));
if (extrude > 0) {
conrtolPoints.add(new Vector3f(0, extrude - handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude, -bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude + handlerLength, -bevelDepth));
}
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, 0));
if (cyclic) {
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, handlerLength));
conrtolPoints.add(new Vector3f(0, extrude + handlerLength, bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude, bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude - handlerLength, bevelDepth));
if (extrude > 0) {
conrtolPoints.add(new Vector3f(0, -extrude + handlerLength, bevelDepth));
conrtolPoints.add(new Vector3f(0, -extrude, bevelDepth));
conrtolPoints.add(new Vector3f(0, -extrude - handlerLength, bevelDepth));
}
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, handlerLength));
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, 0));
}
} else {
if (extrude > 0) {
if (isBack) {
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, 0));
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, -extrude - handlerLength, -bevelDepth));
}
conrtolPoints.add(new Vector3f(0, -extrude, -bevelDepth));
conrtolPoints.add(new Vector3f(0, -extrude + handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude - handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude, -bevelDepth));
if (isFront) {
conrtolPoints.add(new Vector3f(0, extrude + handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, 0));
}
} else {
if (isFront && isBack) {
conrtolPoints.add(new Vector3f(0, -bevelDepth, 0));
conrtolPoints.add(new Vector3f(0, -bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, -handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth));
conrtolPoints.add(new Vector3f(0, handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, bevelDepth, 0));
} else {
if (isBack) {
conrtolPoints.add(new Vector3f(0, -bevelDepth, 0));
conrtolPoints.add(new Vector3f(0, -bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, -handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth));
} else {
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth));
conrtolPoints.add(new Vector3f(0, handlerLength, -bevelDepth));
conrtolPoints.add(new Vector3f(0, bevelDepth, -handlerLength));
conrtolPoints.add(new Vector3f(0, bevelDepth, 0));
}
}
}
}
bevelCurve = new Curve(new Spline(SplineType.Bezier, conrtolPoints, 0, false), bevResol);
} else if (extrude > 0.0f) {
Spline bevelSpline = new Spline(SplineType.Linear, new Vector3f[] { new Vector3f(0, extrude, 0), new Vector3f(0, -extrude, 0) }, 1, false);
bevelCurve = new Curve(bevelSpline, bevResol);
}
if (bevelCurve != null) {
bevelObject = new CurvesTemporalMesh(blenderContext);
FloatBuffer vertsBuffer = (FloatBuffer) bevelCurve.getBuffer(Type.Position).getData();
Vector3f[] verts = BufferUtils.getVector3Array(vertsBuffer);
if (cyclic) {// get rid of the last vertex which is identical to the first one
verts = Arrays.copyOf(verts, verts.length - 1);
}
bevelObject.beziers.add(new BezierLine(verts, 0, false, cyclic));
}
}
return bevelObject;
}
private List<BezierLine> getScaledBeziers() {
if (scale.equals(Vector3f.UNIT_XYZ)) {
return beziers;
}
List<BezierLine> result = new ArrayList<BezierLine>();
for (BezierLine bezierLine : beziers) {
result.add(bezierLine.scale(scale));
}
return result;
}
/**
* This method applies bevel and taper objects to the curve.
* @param curve
* the curve we apply the objects to
* @param bevelObject
* the bevel object
* @param taperObject
* the taper object
* @param blenderContext
* the blender context
* @return a list of geometries representing the beveled and/or tapered curve
* @throws BlenderFileException
* an exception is thrown when problems with reading occur
*/
private CurvesTemporalMesh applyBevelAndTaper(CurvesTemporalMesh curve, CurvesTemporalMesh bevelObject, CurvesTemporalMesh taperObject, BlenderContext blenderContext) throws BlenderFileException {
List<BezierLine> bevelBezierLines = bevelObject.getScaledBeziers();
List<BezierLine> curveLines = curve.beziers;
if (bevelBezierLines.size() == 0 || curveLines.size() == 0) {
return null;
}
CurvesTemporalMesh result = new CurvesTemporalMesh(blenderContext);
for (BezierLine curveLine : curveLines) {
Vector3f[] curveLineVertices = curveLine.getVertices(bevelStart, bevelEnd);
for (BezierLine bevelBezierLine : bevelBezierLines) {
CurvesTemporalMesh partResult = new CurvesTemporalMesh(blenderContext);
Vector3f[] bevelLineVertices = bevelBezierLine.getVertices();
List<Vector3f[]> bevels = new ArrayList<Vector3f[]>();
Vector3f[] bevelPoints = curvesHelper.transformToFirstLineOfBevelPoints(bevelLineVertices, curveLineVertices[0], curveLineVertices[1]);
bevels.add(bevelPoints);
for (int i = 1; i < curveLineVertices.length - 1; ++i) {
bevelPoints = curvesHelper.transformBevel(bevelPoints, curveLineVertices[i - 1], curveLineVertices[i], curveLineVertices[i + 1]);
bevels.add(bevelPoints);
}
bevelPoints = curvesHelper.transformBevel(bevelPoints, curveLineVertices[curveLineVertices.length - 2], curveLineVertices[curveLineVertices.length - 1], null);
bevels.add(bevelPoints);
Vector3f subtractResult = new Vector3f();
if (bevels.size() > 2) {
// changing the first and last bevel so that they are parallel to their neighbours (blender works this way)
// notice this implicates that the distances of every corresponding point in the two bevels must be identical and
// equal to the distance between the points on curve that define the bevel position
// so instead doing complicated rotations on each point we will simply properly translate each of them
int[][] pointIndexes = new int[][] { { 0, 1 }, { curveLineVertices.length - 1, curveLineVertices.length - 2 } };
for (int[] indexes : pointIndexes) {
float distance = curveLineVertices[indexes[1]].subtract(curveLineVertices[indexes[0]], subtractResult).length();
Vector3f[] bevel = bevels.get(indexes[0]);
Vector3f[] nextBevel = bevels.get(indexes[1]);
for (int i = 0; i < bevel.length; ++i) {
float d = bevel[i].subtract(nextBevel[i], subtractResult).length();
subtractResult.normalizeLocal().multLocal(distance - d);
bevel[i].addLocal(subtractResult);
}
}
}
if (taperObject != null) {
float curveLength = curveLine.getLength(), lengthAlongCurve = bevelStart;
for (int i = 0; i < curveLineVertices.length; ++i) {
if (i > 0) {
lengthAlongCurve += curveLineVertices[i].subtract(curveLineVertices[i - 1], subtractResult).length();
}
float taperScale = -taperObject.getValueAlongCurve(lengthAlongCurve / curveLength).z * taperObject.scale.z;
if (taperScale != 1) {
this.applyScale(bevels.get(i), curveLineVertices[i], taperScale);
}
}
}
// adding vertices to the part result
for (Vector3f[] bevel : bevels) {
for (Vector3f d : bevel) {
partResult.getVertices().add(d);
}
}
// preparing faces for the part result (each face is a quad)
int bevelVertCount = bevelPoints.length;
for (int i = 0; i < bevels.size() - 1; ++i) {
for (int j = 0; j < bevelVertCount - 1; ++j) {
Integer[] indexes = new Integer[] { i * bevelVertCount + j + 1, (i + 1) * bevelVertCount + j + 1, (i + 1) * bevelVertCount + j, i * bevelVertCount + j };
partResult.getFaces().add(new Face(indexes, curveLine.isSmooth(), curveLine.getMaterialNumber(), null, null, partResult));
partResult.getEdges().add(new Edge(indexes[0], indexes[1], 0, true, partResult));
partResult.getEdges().add(new Edge(indexes[1], indexes[2], 0, true, partResult));
partResult.getEdges().add(new Edge(indexes[2], indexes[3], 0, true, partResult));
partResult.getEdges().add(new Edge(indexes[3], indexes[0], 0, true, partResult));
}
if (bevelBezierLine.isCyclic()) {
int j = bevelVertCount - 1;
Integer[] indexes = new Integer[] { i * bevelVertCount, (i + 1) * bevelVertCount, (i + 1) * bevelVertCount + j, i * bevelVertCount + j };
partResult.getFaces().add(new Face(indexes, curveLine.isSmooth(), curveLine.getMaterialNumber(), null, null, partResult));
partResult.getEdges().add(new Edge(indexes[0], indexes[1], 0, true, partResult));
partResult.getEdges().add(new Edge(indexes[1], indexes[2], 0, true, partResult));
partResult.getEdges().add(new Edge(indexes[2], indexes[3], 0, true, partResult));
partResult.getEdges().add(new Edge(indexes[3], indexes[0], 0, true, partResult));
}
}
partResult.generateNormals();
if (fillCaps) {// caps in blender behave as if they weren't affected by the smooth factor
// START CAP
Vector3f[] cap = bevels.get(0);
List<Integer> capIndexes = new ArrayList<Integer>(cap.length);
Vector3f capNormal = curveLineVertices[0].subtract(curveLineVertices[1]).normalizeLocal();
for (int i = 0; i < cap.length; ++i) {
capIndexes.add(partResult.getVertices().size());
partResult.getVertices().add(cap[i]);
partResult.getNormals().add(capNormal);
}
Collections.reverse(capIndexes);// the indexes ned to be reversed for the face to have fron face outside the beveled line
partResult.getFaces().add(new Face(capIndexes.toArray(new Integer[capIndexes.size()]), false, curveLine.getMaterialNumber(), null, null, partResult));
for (int i = 1; i < capIndexes.size(); ++i) {
partResult.getEdges().add(new Edge(capIndexes.get(i - 1), capIndexes.get(i), 0, true, partResult));
}
// END CAP
cap = bevels.get(bevels.size() - 1);
capIndexes.clear();
capNormal = curveLineVertices[curveLineVertices.length - 1].subtract(curveLineVertices[curveLineVertices.length - 2]).normalizeLocal();
for (int i = 0; i < cap.length; ++i) {
capIndexes.add(partResult.getVertices().size());
partResult.getVertices().add(cap[i]);
partResult.getNormals().add(capNormal);
}
partResult.getFaces().add(new Face(capIndexes.toArray(new Integer[capIndexes.size()]), false, curveLine.getMaterialNumber(), null, null, partResult));
for (int i = 1; i < capIndexes.size(); ++i) {
partResult.getEdges().add(new Edge(capIndexes.get(i - 1), capIndexes.get(i), 0, true, partResult));
}
}
result.append(partResult);
}
}
return result;
}
/**
* The method generates normals for the curve. If any normals were already stored they are discarded.
*/
private void generateNormals() {
Map<Integer, Vector3f> normalMap = new TreeMap<Integer, Vector3f>();
for (Face face : faces) {
// the first 3 verts are enough here (all faces are triangles except for the caps, but those are fully flat anyway)
int index1 = face.getIndexes().get(0);
int index2 = face.getIndexes().get(1);
int index3 = face.getIndexes().get(2);
Vector3f n = FastMath.computeNormal(vertices.get(index1), vertices.get(index2), vertices.get(index3));
for (int index : face.getIndexes()) {
Vector3f normal = normalMap.get(index);
if (normal == null) {
normalMap.put(index, n.clone());
} else {
normal.addLocal(n).normalizeLocal();
}
}
}
normals.clear();
Collections.addAll(normals, new Vector3f[normalMap.size()]);
for (Entry<Integer, Vector3f> entry : normalMap.entrySet()) {
normals.set(entry.getKey(), entry.getValue());
}
}
/**
* the method applies scale for the given bevel points. The points table is
* being modified so expect your result there.
*
* @param points
* the bevel points
* @param centerPoint
* the center point of the bevel
* @param scale
* the scale to be applied
*/
private void applyScale(Vector3f[] points, Vector3f centerPoint, float scale) {
Vector3f taperScaleVector = new Vector3f();
for (Vector3f p : points) {
taperScaleVector.set(centerPoint).subtractLocal(p).multLocal(1 - scale);
p.addLocal(taperScaleVector);
}
}
/**
* A helper class that represents a single bezier line. It consists of Edge's and allows to
* get a subline of a length of the line.
*
* @author Marcin Roguski (Kaelthas)
*/
public static class BezierLine {
/** The edges of the bezier line. */
private Vector3f[] vertices;
/** The material number of the line. */
private int materialNumber;
/** Indicates if the line is smooth of flat. */
private boolean smooth;
/** The length of the line. */
private float length;
/** Indicates if the current line is cyclic or not. */
private boolean cyclic;
public BezierLine(Vector3f[] vertices, int materialNumber, boolean smooth, boolean cyclik) {
this.vertices = vertices;
this.materialNumber = materialNumber;
this.smooth = smooth;
cyclic = cyclik;
this.recomputeLength();
}
public BezierLine scale(Vector3f scale) {
BezierLine result = new BezierLine(vertices, materialNumber, smooth, cyclic);
result.vertices = new Vector3f[vertices.length];
for (int i = 0; i < vertices.length; ++i) {
result.vertices[i] = vertices[i].mult(scale);
}
result.recomputeLength();
return result;
}
public void removeLastVertex() {
Vector3f[] newVertices = new Vector3f[vertices.length - 1];
for (int i = 0; i < vertices.length - 1; ++i) {
newVertices[i] = vertices[i];
}
vertices = newVertices;
this.recomputeLength();
}
private void recomputeLength() {
length = 0;
for (int i = 1; i < vertices.length; ++i) {
length += vertices[i - 1].distance(vertices[i]);
}
if (cyclic) {
// if the first vertex is repeated at the end the distance will be = 0 so it won't affect the result, and if it is not repeated
// then it is necessary to add the length between the last and the first vertex
length += vertices[vertices.length - 1].distance(vertices[0]);
}
}
public Vector3f[] getVertices() {
return this.getVertices(0, 1);
}
public Vector3f[] getVertices(float startSlice, float endSlice) {
if (startSlice == 0 && endSlice == 1) {
return vertices;
}
List<Vector3f> result = new ArrayList<Vector3f>();
float length = this.getLength(), temp = 0;
float startSliceLength = length * startSlice;
float endSliceLength = length * endSlice;
int index = 1;
if (startSlice > 0) {
while (temp < startSliceLength) {
Vector3f v1 = vertices[index - 1];
Vector3f v2 = vertices[index++];
float edgeLength = v1.distance(v2);
temp += edgeLength;
if (temp == startSliceLength) {
result.add(v2);
} else if (temp > startSliceLength) {
result.add(v1.subtract(v2).normalizeLocal().multLocal(temp - startSliceLength).addLocal(v2));
}
}
}
if (endSlice < 1) {
if (index == vertices.length) {
Vector3f v1 = vertices[vertices.length - 2];
Vector3f v2 = vertices[vertices.length - 1];
result.add(v1.subtract(v2).normalizeLocal().multLocal(length - endSliceLength).addLocal(v2));
} else {
for (int i = index; i < vertices.length && temp < endSliceLength; ++i) {
Vector3f v1 = vertices[index - 1];
Vector3f v2 = vertices[index++];
temp += v1.distance(v2);
if (temp == endSliceLength) {
result.add(v2);
} else if (temp > endSliceLength) {
result.add(v1.subtract(v2).normalizeLocal().multLocal(temp - startSliceLength).addLocal(v2));
}
}
}
} else {
result.addAll(Arrays.asList(Arrays.copyOfRange(vertices, index, vertices.length)));
}
return result.toArray(new Vector3f[result.size()]);
}
/**
* The method computes the value of a point at the certain relational distance from its beginning.
* @param alongRatio
* the relative distance along the curve; should be a value between 0 and 1 inclusive;
* if the value exceeds the boundaries it is truncated to them
* @return computed value along the curve
*/
public Vector3f getValueAlongCurve(float alongRatio) {
alongRatio = FastMath.clamp(alongRatio, 0, 1);
Vector3f result = new Vector3f();
float probeLength = this.getLength() * alongRatio;
float length = 0;
for (int i = 1; i < vertices.length; ++i) {
float edgeLength = vertices[i].distance(vertices[i - 1]);
if (length + edgeLength > probeLength) {
float ratioAlongEdge = (probeLength - length) / edgeLength;
return FastMath.interpolateLinear(ratioAlongEdge, vertices[i - 1], vertices[i]);
} else if (length + edgeLength == probeLength) {
return vertices[i];
}
length += edgeLength;
}
return result;
}
/**
* @return the material number of this bezier line
*/
public int getMaterialNumber() {
return materialNumber;
}
/**
* @return indicates if the line is smooth of flat
*/
public boolean isSmooth() {
return smooth;
}
/**
* @return the length of this bezier line
*/
public float getLength() {
return length;
}
/**
* @return indicates if the current line is cyclic or not
*/
public boolean isCyclic() {
return cyclic;
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,583 @@
/*
* Copyright (c) 2009-2020 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.math;
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.Quaternion;
/**
* <code>DQuaternion</code> defines a single example of a more general class of
* hypercomplex numbers. DQuaternions extends a rotation in three dimensions to a
* rotation in four dimensions. This avoids "gimbal lock" and allows for smooth
* continuous rotation.
*
* <code>DQuaternion</code> is defined by four double point numbers: {x y z w}.
*
* This class's only purpose is to give better accuracy in floating point operations during computations.
* This is made by copying the original Quaternion class from jme3 core and leaving only required methods and basic computation methods, so that
* the class is smaller and easier to maintain.
* Should any other methods be needed, they will be added.
*
* @author Mark Powell
* @author Joshua Slack
* @author Marcin Roguski (Kaelthas)
*/
public final class DQuaternion implements Savable, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 5009180713885017539L;
/**
* Represents the identity quaternion rotation (0, 0, 0, 1).
*/
public static final DQuaternion IDENTITY = new DQuaternion();
public static final DQuaternion DIRECTION_Z = new DQuaternion();
public static final DQuaternion ZERO = new DQuaternion(0, 0, 0, 0);
protected double x, y, z, w = 1;
/**
* Constructor instantiates a new <code>DQuaternion</code> object
* initializing all values to zero, except w which is initialized to 1.
*
*/
public DQuaternion() {
}
/**
* Constructor instantiates a new <code>DQuaternion</code> object from the
* given list of parameters.
*
* @param x
* the x value of the quaternion.
* @param y
* the y value of the quaternion.
* @param z
* the z value of the quaternion.
* @param w
* the w value of the quaternion.
*/
public DQuaternion(double x, double y, double z, double w) {
this.set(x, y, z, w);
}
public DQuaternion(Quaternion q) {
this(q.getX(), q.getY(), q.getZ(), q.getW());
}
public Quaternion toQuaternion() {
return new Quaternion((float) x, (float) y, (float) z, (float) w);
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
public double getW() {
return w;
}
/**
* sets the data in a <code>DQuaternion</code> object from the given list
* of parameters.
*
* @param x
* the x value of the quaternion.
* @param y
* the y value of the quaternion.
* @param z
* the z value of the quaternion.
* @param w
* the w value of the quaternion.
* @return this
*/
public DQuaternion set(double x, double y, double z, double w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
return this;
}
/**
* Sets the data in this <code>DQuaternion</code> object to be equal to the
* passed <code>DQuaternion</code> object. The values are copied producing
* a new object.
*
* @param q
* The DQuaternion to copy values from.
* @return this
*/
public DQuaternion set(DQuaternion q) {
x = q.x;
y = q.y;
z = q.z;
w = q.w;
return this;
}
/**
* Sets this DQuaternion to {0, 0, 0, 1}. Same as calling set(0,0,0,1).
*/
public void loadIdentity() {
x = y = z = 0;
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
* by an angle and an axis of rotation. This method creates an object, so
* use fromAngleNormalAxis if your axis is already normalized.
*
* @param angle
* the angle to rotate (in radians).
* @param axis
* the axis of rotation.
* @return this quaternion
*/
public DQuaternion fromAngleAxis(double angle, Vector3d axis) {
Vector3d normAxis = axis.normalize();
this.fromAngleNormalAxis(angle, normAxis);
return this;
}
/**
* <code>fromAngleNormalAxis</code> sets this quaternion to the values
* specified by an angle and a normalized axis of rotation.
*
* @param angle
* the angle to rotate (in radians).
* @param axis
* the axis of rotation (already normalized).
*/
public DQuaternion fromAngleNormalAxis(double angle, Vector3d axis) {
if (axis.x == 0 && axis.y == 0 && axis.z == 0) {
this.loadIdentity();
} else {
double halfAngle = 0.5f * angle;
double sin = Math.sin(halfAngle);
w = Math.cos(halfAngle);
x = sin * axis.x;
y = sin * axis.y;
z = sin * axis.z;
}
return this;
}
/**
* <code>add</code> adds the values of this quaternion to those of the
* parameter quaternion. The result is returned as a new quaternion.
*
* @param q
* the quaternion to add to this.
* @return the new quaternion.
*/
public DQuaternion add(DQuaternion q) {
return new DQuaternion(x + q.x, y + q.y, z + q.z, w + q.w);
}
/**
* <code>add</code> adds the values of this quaternion to those of the
* parameter quaternion. The result is stored in this DQuaternion.
*
* @param q
* the quaternion to add to this.
* @return This DQuaternion after addition.
*/
public DQuaternion addLocal(DQuaternion q) {
x += q.x;
y += q.y;
z += q.z;
w += q.w;
return this;
}
/**
* <code>subtract</code> subtracts the values of the parameter quaternion
* from those of this quaternion. The result is returned as a new
* quaternion.
*
* @param q
* the quaternion to subtract from this.
* @return the new quaternion.
*/
public DQuaternion subtract(DQuaternion q) {
return new DQuaternion(x - q.x, y - q.y, z - q.z, w - q.w);
}
/**
* <code>subtract</code> subtracts the values of the parameter quaternion
* from those of this quaternion. The result is stored in this DQuaternion.
*
* @param q
* the quaternion to subtract from this.
* @return This DQuaternion after subtraction.
*/
public DQuaternion subtractLocal(DQuaternion q) {
x -= q.x;
y -= q.y;
z -= q.z;
w -= q.w;
return this;
}
/**
* <code>mult</code> multiplies this quaternion by a parameter quaternion.
* The result is returned as a new quaternion. It should be noted that
* quaternion multiplication is not commutative so q * p != p * q.
*
* @param q
* the quaternion to multiply this quaternion by.
* @return the new quaternion.
*/
public DQuaternion mult(DQuaternion q) {
return this.mult(q, null);
}
/**
* <code>mult</code> multiplies this quaternion by a parameter quaternion.
* The result is returned as a new quaternion. It should be noted that
* quaternion multiplication is not commutative so q * p != p * q.
*
* It IS safe for q and res to be the same object.
* It IS NOT safe for this and res to be the same object.
*
* @param q
* the quaternion to multiply this quaternion by.
* @param res
* the quaternion to store the result in.
* @return the new quaternion.
*/
public DQuaternion mult(DQuaternion q, DQuaternion res) {
if (res == null) {
res = new DQuaternion();
}
double qw = q.w, qx = q.x, qy = q.y, qz = q.z;
res.x = x * qw + y * qz - z * qy + w * qx;
res.y = -x * qz + y * qw + z * qx + w * qy;
res.z = x * qy - y * qx + z * qw + w * qz;
res.w = -x * qx - y * qy - z * qz + w * qw;
return res;
}
/**
* <code>mult</code> multiplies this quaternion by a parameter vector. The
* result is returned as a new vector.
*
* @param v
* the vector to multiply this quaternion by.
* @return the new vector.
*/
public Vector3d mult(Vector3d v) {
return this.mult(v, null);
}
/**
* Multiplies this DQuaternion by the supplied quaternion. The result is
* stored in this DQuaternion, which is also returned for chaining. Similar
* to this *= q.
*
* @param q
* The DQuaternion to multiply this one by.
* @return This DQuaternion, after multiplication.
*/
public DQuaternion multLocal(DQuaternion q) {
double x1 = x * q.w + y * q.z - z * q.y + w * q.x;
double y1 = -x * q.z + y * q.w + z * q.x + w * q.y;
double z1 = x * q.y - y * q.x + z * q.w + w * q.z;
w = -x * q.x - y * q.y - z * q.z + w * q.w;
x = x1;
y = y1;
z = z1;
return this;
}
/**
* <code>mult</code> multiplies this quaternion by a parameter vector. The
* result is returned as a new vector.
*
* @param v
* the vector to multiply this quaternion by.
* @param store
* the vector to store the result in. It IS safe for v and store
* to be the same object.
* @return the result vector.
*/
public Vector3d mult(Vector3d v, Vector3d store) {
if (store == null) {
store = new Vector3d();
}
if (v.x == 0 && v.y == 0 && v.z == 0) {
store.set(0, 0, 0);
} else {
double vx = v.x, vy = v.y, vz = v.z;
store.x = w * w * vx + 2 * y * w * vz - 2 * z * w * vy + x * x * vx + 2 * y * x * vy + 2 * z * x * vz - z * z * vx - y * y * vx;
store.y = 2 * x * y * vx + y * y * vy + 2 * z * y * vz + 2 * w * z * vx - z * z * vy + w * w * vy - 2 * x * w * vz - x * x * vy;
store.z = 2 * x * z * vx + 2 * y * z * vy + z * z * vz - 2 * w * y * vx - y * y * vz + 2 * w * x * vy - x * x * vz + w * w * vz;
}
return store;
}
/**
*
* <code>toString</code> creates the string representation of this <code>DQuaternion</code>. The values of the quaternion are displaced (x,
* y, z, w), in the following manner: <br>
* (x, y, z, w)
*
* @return the string representation of this object.
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "(" + x + ", " + y + ", " + z + ", " + w + ")";
}
/**
* <code>equals</code> determines if two quaternions are logically equal,
* that is, if the values of (x, y, z, w) are the same for both quaternions.
*
* @param o
* the object to compare for equality
* @return true if they are equal, false otherwise.
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof DQuaternion)) {
return false;
}
if (this == o) {
return true;
}
DQuaternion comp = (DQuaternion) o;
if (Double.compare(x, comp.x) != 0) {
return false;
}
if (Double.compare(y, comp.y) != 0) {
return false;
}
if (Double.compare(z, comp.z) != 0) {
return false;
}
if (Double.compare(w, comp.w) != 0) {
return false;
}
return true;
}
/**
*
* <code>hashCode</code> returns the hash code value as an integer and is
* supported for the benefit of hashing based collection classes such as
* Hashtable, HashMap, HashSet etc.
*
* @return the hashcode for this instance of DQuaternion.
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
long hash = 37;
hash = 37 * hash + Double.doubleToLongBits(x);
hash = 37 * hash + Double.doubleToLongBits(y);
hash = 37 * hash + Double.doubleToLongBits(z);
hash = 37 * hash + Double.doubleToLongBits(w);
return (int) hash;
}
@Override
public void write(JmeExporter e) throws IOException {
OutputCapsule cap = e.getCapsule(this);
cap.write(x, "x", 0);
cap.write(y, "y", 0);
cap.write(z, "z", 0);
cap.write(w, "w", 1);
}
@Override
public void read(JmeImporter e) throws IOException {
InputCapsule cap = e.getCapsule(this);
x = cap.readFloat("x", 0);
y = cap.readFloat("y", 0);
z = cap.readFloat("z", 0);
w = cap.readFloat("w", 1);
}
@Override
public DQuaternion clone() {
try {
return (DQuaternion) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // can not happen
}
}
}

@ -0,0 +1,190 @@
/*
* Copyright (c) 2009-2020 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.math;
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>
* <br>
* Represents a translation, rotation and scale in one object.
*
* This class's only purpose is to give better accuracy in floating point operations during computations.
* This is made by copying the original Transfrom class from jme3 core and removing unnecessary methods so that
* the class is smaller and easier to maintain.
* Should any other methods be needed, they will be added.
*
* @author Jack Lindamood
* @author Joshua Slack
* @author Marcin Roguski (Kaelthas)
*/
public final class DTransform implements Savable, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 7812915425940606722L;
private DQuaternion rotation;
private Vector3d translation;
private Vector3d scale;
public DTransform() {
translation = new Vector3d();
rotation = new DQuaternion();
scale = new Vector3d();
}
public DTransform(Transform transform) {
translation = new Vector3d(transform.getTranslation());
rotation = new DQuaternion(transform.getRotation());
scale = new Vector3d(transform.getScale());
}
public Transform toTransform() {
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.
* @param trans
* The new translation for this matrix.
* @return this
*/
public DTransform setTranslation(Vector3d trans) {
translation.set(trans);
return this;
}
/**
* Sets this rotation to the given DQuaternion value.
* @param rot
* The new rotation for this matrix.
* @return this
*/
public DTransform setRotation(DQuaternion rot) {
rotation.set(rot);
return this;
}
/**
* Sets this scale to the given value.
* @param scale
* The new scale for this matrix.
* @return this
*/
public DTransform setScale(Vector3d scale) {
this.scale.set(scale);
return this;
}
/**
* Sets this scale to the given value.
* @param scale
* The new scale for this matrix.
* @return this
*/
public DTransform setScale(float scale) {
this.scale.set(scale, scale, scale);
return this;
}
/**
* Return the translation vector in this matrix.
* @return translation vector.
*/
public Vector3d getTranslation() {
return translation;
}
/**
* Return the rotation quaternion in this matrix.
* @return rotation quaternion.
*/
public DQuaternion getRotation() {
return rotation;
}
/**
* Return the scale vector in this matrix.
* @return scale vector.
*/
public Vector3d getScale() {
return scale;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "[ " + translation.x + ", " + translation.y + ", " + translation.z + "]\n" + "[ " + rotation.x + ", " + rotation.y + ", " + rotation.z + ", " + rotation.w + "]\n" + "[ " + scale.x + " , " + scale.y + ", " + scale.z + "]";
}
@Override
public void write(JmeExporter e) throws IOException {
OutputCapsule capsule = e.getCapsule(this);
capsule.write(rotation, "rot", new DQuaternion());
capsule.write(translation, "translation", Vector3d.ZERO);
capsule.write(scale, "scale", Vector3d.UNIT_XYZ);
}
@Override
public void read(JmeImporter e) throws IOException {
InputCapsule capsule = e.getCapsule(this);
rotation = (DQuaternion) capsule.readSavable("rot", new DQuaternion());
translation = (Vector3d) capsule.readSavable("translation", Vector3d.ZERO);
scale = (Vector3d) capsule.readSavable("scale", Vector3d.UNIT_XYZ);
}
@Override
public DTransform clone() {
try {
DTransform tq = (DTransform) super.clone();
tq.rotation = rotation.clone();
tq.scale = scale.clone();
tq.translation = translation.clone();
return tq;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}

@ -0,0 +1,210 @@
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;
}
/**
* Retrieves the scale vector from the matrix and stores it into a given
* vector.
*/
public Vector3d toScaleVector() {
Vector3d result = new Vector3d();
this.toScaleVector(result);
return result;
}
/**
* Retrieves the scale vector from the matrix and stores it into a given
* vector.
*
* @param vector 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);
}
}

@ -0,0 +1,869 @@
/*
* Copyright (c) 2009-2020 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.math;
import java.io.IOException;
import java.io.Serializable;
import java.util.logging.Logger;
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.FastMath;
import com.jme3.math.Vector3f;
/*
* -- Added *Local methods to cut down on object creation - JS
*/
/**
* <code>Vector3d</code> defines a Vector for a three float value tuple. <code>Vector3d</code> can represent any three dimensional value, such as a
* vertex, a normal, etc. Utility methods are also included to aid in
* mathematical calculations.
*
* This class's only purpose is to give better accuracy in floating point operations during computations.
* This is made by copying the original Vector3f class from jme3 core and leaving only required methods and basic computation methods, so that
* the class is smaller and easier to maintain.
* Should any other methods be needed, they will be added.
*
* @author Mark Powell
* @author Joshua Slack
* @author Marcin Roguski (Kaelthas)
*/
public final class Vector3d implements Savable, Cloneable, Serializable {
private static final long serialVersionUID = 3090477054277293078L;
private static final Logger LOGGER = Logger.getLogger(Vector3d.class.getName());
public final static Vector3d ZERO = new Vector3d();
public final static Vector3d UNIT_XYZ = new Vector3d(1, 1, 1);
public final static Vector3d UNIT_X = new Vector3d(1, 0, 0);
public final static Vector3d UNIT_Y = new Vector3d(0, 1, 0);
public final static Vector3d UNIT_Z = new Vector3d(0, 0, 1);
/**
* the x value of the vector.
*/
public double x;
/**
* the y value of the vector.
*/
public double y;
/**
* the z value of the vector.
*/
public double z;
/**
* Constructor instantiates a new <code>Vector3d</code> with default
* values of (0,0,0).
*
*/
public Vector3d() {
}
/**
* Constructor instantiates a new <code>Vector3d</code> with provides
* values.
*
* @param x
* the x value of the vector.
* @param y
* the y value of the vector.
* @param z
* the z value of the vector.
*/
public Vector3d(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Constructor instantiates a new <code>Vector3d</code> that is a copy
* of the provided vector
* @param vector3f
* The Vector3f to copy
*/
public Vector3d(Vector3f vector3f) {
this(vector3f.x, vector3f.y, vector3f.z);
}
public Vector3f toVector3f() {
return new Vector3f((float) x, (float) y, (float) z);
}
/**
* <code>set</code> sets the x,y,z values of the vector based on passed
* parameters.
*
* @param x
* the x value of the vector.
* @param y
* the y value of the vector.
* @param z
* the z value of the vector.
* @return this vector
*/
public Vector3d set(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
/**
* <code>set</code> sets the x,y,z values of the vector by copying the
* supplied vector.
*
* @param vect
* the vector to copy.
* @return this vector
*/
public Vector3d set(Vector3d vect) {
return this.set(vect.x, vect.y, vect.z);
}
/**
*
* <code>add</code> adds a provided vector to this vector creating a
* resultant vector which is returned. If the provided vector is null, null
* is returned.
*
* @param vec
* the vector to add to this.
* @return the resultant vector.
*/
public Vector3d add(Vector3d vec) {
if (null == vec) {
LOGGER.warning("Provided vector is null, null returned.");
return null;
}
return new Vector3d(x + vec.x, y + vec.y, z + vec.z);
}
/**
*
* <code>add</code> adds the values of a provided vector storing the
* values in the supplied vector.
*
* @param vec
* the vector to add to this
* @param result
* the vector to store the result in
* @return result returns the supplied result vector.
*/
public Vector3d add(Vector3d vec, Vector3d result) {
result.x = x + vec.x;
result.y = y + vec.y;
result.z = z + vec.z;
return result;
}
/**
* <code>addLocal</code> adds a provided vector to this vector internally,
* and returns a handle to this vector for easy chaining of calls. If the
* provided vector is null, null is returned.
*
* @param vec
* the vector to add to this vector.
* @return this
*/
public Vector3d addLocal(Vector3d vec) {
if (null == vec) {
LOGGER.warning("Provided vector is null, null returned.");
return null;
}
x += vec.x;
y += vec.y;
z += vec.z;
return this;
}
/**
*
* <code>add</code> adds the provided values to this vector, creating a
* new vector that is then returned.
*
* @param addX
* the x value to add.
* @param addY
* the y value to add.
* @param addZ
* the z value to add.
* @return the result vector.
*/
public Vector3d add(double addX, double addY, double addZ) {
return new Vector3d(x + addX, y + addY, z + addZ);
}
/**
* <code>addLocal</code> adds the provided values to this vector
* internally, and returns a handle to this vector for easy chaining of
* calls.
*
* @param addX
* value to add to x
* @param addY
* value to add to y
* @param addZ
* value to add to z
* @return this
*/
public Vector3d addLocal(double addX, double addY, double addZ) {
x += addX;
y += addY;
z += addZ;
return this;
}
/**
*
* <code>scaleAdd</code> multiplies this vector by a scalar then adds the
* given Vector3d.
*
* @param scalar
* the value to multiply this vector by.
* @param add
* the value to add
*/
public Vector3d scaleAdd(double scalar, Vector3d add) {
x = x * scalar + add.x;
y = y * scalar + add.y;
z = z * scalar + add.z;
return this;
}
/**
*
* <code>scaleAdd</code> multiplies the given vector by a scalar then adds
* the given vector.
*
* @param scalar
* the value to multiply this vector by.
* @param mult
* the value to multiply the scalar by
* @param add
* the value to add
*/
public Vector3d scaleAdd(double scalar, Vector3d mult, Vector3d add) {
x = mult.x * scalar + add.x;
y = mult.y * scalar + add.y;
z = mult.z * scalar + add.z;
return this;
}
/**
*
* <code>dot</code> calculates the dot product of this vector with a
* provided vector. If the provided vector is null, 0 is returned.
*
* @param vec
* the vector to dot with this vector.
* @return the resultant dot product of this vector and a given vector.
*/
public double dot(Vector3d vec) {
if (null == vec) {
LOGGER.warning("Provided vector is null, 0 returned.");
return 0;
}
return x * vec.x + y * vec.y + z * vec.z;
}
/**
* <code>cross</code> calculates the cross product of this vector with a
* parameter vector v.
*
* @param v
* the vector to take the cross product of with this.
* @return the cross product vector.
*/
public Vector3d cross(Vector3d v) {
return this.cross(v, null);
}
/**
* <code>cross</code> calculates the cross product of this vector with a
* parameter vector v. The result is stored in <code>result</code>
*
* @param v
* the vector to take the cross product of with this.
* @param result
* the vector to store the cross product result.
* @return result, after receiving the cross product vector.
*/
public Vector3d cross(Vector3d v, Vector3d result) {
return this.cross(v.x, v.y, v.z, result);
}
/**
* <code>cross</code> calculates the cross product of this vector with a
* parameter vector v. The result is stored in <code>result</code>
*
* @param otherX
* x component of the vector to take the cross product of with this.
* @param otherY
* y component of the vector to take the cross product of with this.
* @param otherZ
* z component of the vector to take the cross product of with this.
* @param result
* the vector to store the cross product result.
* @return result, after receiving the cross product vector.
*/
public Vector3d cross(double otherX, double otherY, double otherZ, Vector3d result) {
if (result == null) {
result = new Vector3d();
}
double resX = y * otherZ - z * otherY;
double resY = z * otherX - x * otherZ;
double resZ = x * otherY - y * otherX;
result.set(resX, resY, resZ);
return result;
}
/**
* <code>crossLocal</code> calculates the cross product of this vector
* with a parameter vector v.
*
* @param v
* the vector to take the cross product of with this.
* @return this.
*/
public Vector3d crossLocal(Vector3d v) {
return this.crossLocal(v.x, v.y, v.z);
}
/**
* <code>crossLocal</code> calculates the cross product of this vector
* with a parameter vector v.
*
* @param otherX
* x component of the vector to take the cross product of with this.
* @param otherY
* y component of the vector to take the cross product of with this.
* @param otherZ
* z component of the vector to take the cross product of with this.
* @return this.
*/
public Vector3d crossLocal(double otherX, double otherY, double otherZ) {
double tempx = y * otherZ - z * otherY;
double tempy = z * otherX - x * otherZ;
z = x * otherY - y * otherX;
x = tempx;
y = tempy;
return this;
}
/**
* <code>length</code> calculates the magnitude of this vector.
*
* @return the length or magnitude of the vector.
*/
public double length() {
return Math.sqrt(this.lengthSquared());
}
/**
* <code>lengthSquared</code> calculates the squared value of the
* magnitude of the vector.
*
* @return the magnitude squared of the vector.
*/
public double lengthSquared() {
return x * x + y * y + z * z;
}
/**
* <code>distanceSquared</code> calculates the distance squared between
* this vector and vector v.
*
* @param v
* the second vector to determine the distance squared.
* @return the distance squared between the two vectors.
*/
public double distanceSquared(Vector3d v) {
double dx = x - v.x;
double dy = y - v.y;
double dz = z - v.z;
return dx * dx + dy * dy + dz * dz;
}
/**
* <code>distance</code> calculates the distance between this vector and
* vector v.
*
* @param v
* the second vector to determine the distance.
* @return the distance between the two vectors.
*/
public double distance(Vector3d v) {
return Math.sqrt(this.distanceSquared(v));
}
/**
*
* <code>mult</code> multiplies this vector by a scalar. The resultant
* vector is returned.
*
* @param scalar
* the value to multiply this vector by.
* @return the new vector.
*/
public Vector3d mult(double scalar) {
return new Vector3d(x * scalar, y * scalar, z * scalar);
}
/**
*
* <code>mult</code> multiplies this vector by a scalar. The resultant
* vector is supplied as the second parameter and returned.
*
* @param scalar
* the scalar to multiply this vector by.
* @param product
* the product to store the result in.
* @return product
*/
public Vector3d mult(double scalar, Vector3d product) {
if (null == product) {
product = new Vector3d();
}
product.x = x * scalar;
product.y = y * scalar;
product.z = z * scalar;
return product;
}
/**
* <code>multLocal</code> multiplies this vector by a scalar internally,
* and returns a handle to this vector for easy chaining of calls.
*
* @param scalar
* the value to multiply this vector by.
* @return this
*/
public Vector3d multLocal(double scalar) {
x *= scalar;
y *= scalar;
z *= scalar;
return this;
}
/**
* <code>multLocal</code> multiplies a provided vector by this vector
* internally, and returns a handle to this vector for easy chaining of
* calls. If the provided vector is null, null is returned.
*
* @param vec
* the vector to multiply by this vector.
* @return this
*/
public Vector3d multLocal(Vector3d vec) {
if (null == vec) {
LOGGER.warning("Provided vector is null, null returned.");
return null;
}
x *= vec.x;
y *= vec.y;
z *= vec.z;
return this;
}
/**
* <code>multLocal</code> multiplies this vector by 3 scalars
* internally, and returns a handle to this vector for easy chaining of
* calls.
*
* @param x
* @param y
* @param z
* @return this
*/
public Vector3d multLocal(double x, double y, double z) {
this.x *= x;
this.y *= y;
this.z *= z;
return this;
}
/**
* <code>multLocal</code> multiplies a provided vector by this vector
* internally, and returns a handle to this vector for easy chaining of
* calls. If the provided vector is null, null is returned.
*
* @param vec
* the vector to mult to this vector.
* @return this
*/
public Vector3d mult(Vector3d vec) {
if (null == vec) {
LOGGER.warning("Provided vector is null, null returned.");
return null;
}
return this.mult(vec, null);
}
/**
* <code>multLocal</code> multiplies a provided vector by this vector
* internally, and returns a handle to this vector for easy chaining of
* calls. If the provided vector is null, null is returned.
*
* @param vec
* the vector to mult to this vector.
* @param store
* result vector (null to create a new vector)
* @return this
*/
public Vector3d mult(Vector3d vec, Vector3d store) {
if (null == vec) {
LOGGER.warning("Provided vector is null, null returned.");
return null;
}
if (store == null) {
store = new Vector3d();
}
return store.set(x * vec.x, y * vec.y, z * vec.z);
}
/**
* <code>divide</code> divides the values of this vector by a scalar and
* returns the result. The values of this vector remain untouched.
*
* @param scalar
* the value to divide this vectors attributes by.
* @return the result <code>Vector</code>.
*/
public Vector3d divide(double scalar) {
scalar = 1f / scalar;
return new Vector3d(x * scalar, y * scalar, z * scalar);
}
/**
* <code>divideLocal</code> divides this vector by a scalar internally,
* and returns a handle to this vector for easy chaining of calls. Dividing
* by zero will result in an exception.
*
* @param scalar
* the value to divides this vector by.
* @return this
*/
public Vector3d divideLocal(double scalar) {
scalar = 1f / scalar;
x *= scalar;
y *= scalar;
z *= scalar;
return this;
}
/**
* <code>divide</code> divides the values of this vector by a scalar and
* returns the result. The values of this vector remain untouched.
*
* @param scalar
* the value to divide this vectors attributes by.
* @return the result <code>Vector</code>.
*/
public Vector3d divide(Vector3d scalar) {
return new Vector3d(x / scalar.x, y / scalar.y, z / scalar.z);
}
/**
* <code>divideLocal</code> divides this vector by a scalar internally,
* and returns a handle to this vector for easy chaining of calls. Dividing
* by zero will result in an exception.
*
* @param scalar
* the value to divides this vector by.
* @return this
*/
public Vector3d divideLocal(Vector3d scalar) {
x /= scalar.x;
y /= scalar.y;
z /= scalar.z;
return this;
}
/**
*
* <code>negate</code> returns the negative of this vector. All values are
* negated and set to a new vector.
*
* @return the negated vector.
*/
public Vector3d negate() {
return new Vector3d(-x, -y, -z);
}
/**
*
* <code>negateLocal</code> negates the internal values of this vector.
*
* @return this.
*/
public Vector3d negateLocal() {
x = -x;
y = -y;
z = -z;
return this;
}
/**
*
* <code>subtract</code> subtracts the values of a given vector from those
* of this vector creating a new vector object. If the provided vector is
* null, null is returned.
*
* @param vec
* the vector to subtract from this vector.
* @return the result vector.
*/
public Vector3d subtract(Vector3d vec) {
return new Vector3d(x - vec.x, y - vec.y, z - vec.z);
}
/**
* <code>subtractLocal</code> subtracts a provided vector from this vector
* internally, and returns a handle to this vector for easy chaining of
* calls. If the provided vector is null, null is returned.
*
* @param vec
* the vector to subtract
* @return this
*/
public Vector3d subtractLocal(Vector3d vec) {
if (null == vec) {
LOGGER.warning("Provided vector is null, null returned.");
return null;
}
x -= vec.x;
y -= vec.y;
z -= vec.z;
return this;
}
/**
*
* <code>subtract</code>
*
* @param vec
* the vector to subtract from this
* @param result
* the vector to store the result in
* @return result
*/
public Vector3d subtract(Vector3d vec, Vector3d result) {
if (result == null) {
result = new Vector3d();
}
result.x = x - vec.x;
result.y = y - vec.y;
result.z = z - vec.z;
return result;
}
/**
*
* <code>subtract</code> subtracts the provided values from this vector,
* creating a new vector that is then returned.
*
* @param subtractX
* the x value to subtract.
* @param subtractY
* the y value to subtract.
* @param subtractZ
* the z value to subtract.
* @return the result vector.
*/
public Vector3d subtract(double subtractX, double subtractY, double subtractZ) {
return new Vector3d(x - subtractX, y - subtractY, z - subtractZ);
}
/**
* <code>subtractLocal</code> subtracts the provided values from this vector
* internally, and returns a handle to this vector for easy chaining of
* calls.
*
* @param subtractX
* the x value to subtract.
* @param subtractY
* the y value to subtract.
* @param subtractZ
* the z value to subtract.
* @return this
*/
public Vector3d subtractLocal(double subtractX, double subtractY, double subtractZ) {
x -= subtractX;
y -= subtractY;
z -= subtractZ;
return this;
}
/**
* <code>normalize</code> returns the unit vector of this vector.
*
* @return unit vector of this vector.
*/
public Vector3d normalize() {
double length = x * x + y * y + z * z;
if (length != 1f && length != 0f) {
length = 1.0f / Math.sqrt(length);
return new Vector3d(x * length, y * length, z * length);
}
return this.clone();
}
/**
* <code>normalizeLocal</code> makes this vector into a unit vector of
* itself.
*
* @return this.
*/
public Vector3d normalizeLocal() {
// NOTE: this implementation is more optimized
// than the old jme normalize as this method
// is commonly used.
double length = x * x + y * y + z * z;
if (length != 1f && length != 0f) {
length = 1.0f / Math.sqrt(length);
x *= length;
y *= length;
z *= length;
}
return this;
}
/**
* <code>angleBetween</code> returns (in radians) the angle between two vectors.
* It is assumed that both this vector and the given vector are unit vectors (iow, normalized).
*
* @param otherVector
* a unit vector to find the angle against
* @return the angle in radians.
*/
public double angleBetween(Vector3d otherVector) {
double dot = this.dot(otherVector);
// the vectors are normalized, but if they are parallel then the dot product migh get a value like: 1.000000000000000002
// which is caused by floating point operations; in such case, the acos function will return NaN so we need to clamp this value
dot = FastMath.clamp((float) dot, -1, 1);
return Math.acos(dot);
}
@Override
public Vector3d clone() {
try {
return (Vector3d) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // can not happen
}
}
/**
* are these two vectors the same? they are is they both have the same x,y,
* and z values.
*
* @param o
* the object to compare for equality
* @return true if they are equal
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof Vector3d)) {
return false;
}
if (this == o) {
return true;
}
Vector3d comp = (Vector3d) o;
if (Double.compare(x, comp.x) != 0) {
return false;
}
if (Double.compare(y, comp.y) != 0) {
return false;
}
if (Double.compare(z, comp.z) != 0) {
return false;
}
return true;
}
/**
* <code>hashCode</code> returns a unique code for this vector object based
* on its values. If two vectors are logically equivalent, they will return
* the same hash code value.
* @return the hash code value of this vector.
*/
@Override
public int hashCode() {
long hash = 37;
hash += 37 * hash + Double.doubleToLongBits(x);
hash += 37 * hash + Double.doubleToLongBits(y);
hash += 37 * hash + Double.doubleToLongBits(z);
return (int) hash;
}
/**
* <code>toString</code> returns the string representation of this vector.
* The format is:
*
* org.jme.math.Vector3d [X=XX.XXXX, Y=YY.YYYY, Z=ZZ.ZZZZ]
*
* @return the string representation of this vector.
*/
@Override
public String toString() {
return "(" + x + ", " + y + ", " + z + ")";
}
@Override
public void write(JmeExporter e) throws IOException {
OutputCapsule capsule = e.getCapsule(this);
capsule.write(x, "x", 0);
capsule.write(y, "y", 0);
capsule.write(z, "z", 0);
}
@Override
public void read(JmeImporter e) throws IOException {
InputCapsule capsule = e.getCapsule(this);
x = capsule.readDouble("x", 0);
y = capsule.readDouble("y", 0);
z = capsule.readDouble("z", 0);
}
}

@ -0,0 +1,349 @@
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.math.Vector3d;
import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate;
/**
* A class that represents a single edge between two vertices.
*
* @author Marcin Roguski (Kaelthas)
*/
public class Edge {
private static final Logger LOGGER = Logger.getLogger(Edge.class.getName());
private static final int FLAG_EDGE_NOT_IN_FACE = 0x80;
/** The vertices indexes. */
private int index1, index2;
/** The vertices that can be set if we need and abstract edge outside the mesh (for computations). */
private Vector3f v1, v2;
/** The weight of the edge. */
private float crease;
/** A variable that indicates if this edge belongs to any face or not. */
private boolean inFace;
/** The mesh that owns the edge. */
private TemporalMesh temporalMesh;
public Edge(Vector3f v1, Vector3f v2) {
this.v1 = v1 == null ? new Vector3f() : v1;
this.v2 = v2 == null ? new Vector3f() : v2;
index1 = 0;
index2 = 1;
}
/**
* This constructor only stores the indexes of the vertices. The position vertices should be stored
* outside this class.
* @param index1
* the first index of the edge
* @param index2
* the second index of the edge
* @param crease
* the weight of the face
* @param inFace
* a variable that indicates if this edge belongs to any face or not
*/
public Edge(int index1, int index2, float crease, boolean inFace, TemporalMesh temporalMesh) {
this.index1 = index1;
this.index2 = index2;
this.crease = crease;
this.inFace = inFace;
this.temporalMesh = temporalMesh;
}
@Override
public Edge clone() {
return new Edge(index1, index2, crease, inFace, temporalMesh);
}
/**
* @return the first index of the edge
*/
public int getFirstIndex() {
return index1;
}
/**
* @return the second index of the edge
*/
public int getSecondIndex() {
return index2;
}
/**
* @return the first vertex of the edge
*/
public Vector3f getFirstVertex() {
return temporalMesh == null ? v1 : temporalMesh.getVertices().get(index1);
}
/**
* @return the second vertex of the edge
*/
public Vector3f getSecondVertex() {
return temporalMesh == null ? v2 : temporalMesh.getVertices().get(index2);
}
/**
* Returns the index other than the given.
* @param index
* index of the edge
* @return the remaining index number
*/
public int getOtherIndex(int index) {
if (index == index1) {
return index2;
}
if (index == index2) {
return index1;
}
throw new IllegalArgumentException("Cannot give the other index for [" + index + "] because this index does not exist in edge: " + this);
}
/**
* @return the crease value of the edge (its weight)
*/
public float getCrease() {
return crease;
}
/**
* @return <b>true</b> if the edge is used by at least one face and <b>false</b> otherwise
*/
public boolean isInFace() {
return inFace;
}
/**
* @return the length of the edge
*/
public float getLength() {
return this.getFirstVertex().distance(this.getSecondVertex());
}
/**
* @return the mesh this edge belongs to
*/
public TemporalMesh getTemporalMesh() {
return temporalMesh;
}
/**
* @return the centroid of the edge
*/
public Vector3f computeCentroid() {
return this.getFirstVertex().add(this.getSecondVertex()).divideLocal(2);
}
/**
* Shifts indexes by a given amount.
* @param shift
* how much the indexes should be shifted
* @param predicate
* the predicate that verifies which indexes should be shifted; if null then all will be shifted
*/
public void shiftIndexes(int shift, IndexPredicate predicate) {
if (predicate == null) {
index1 += shift;
index2 += shift;
} else {
index1 += predicate.execute(index1) ? shift : 0;
index2 += predicate.execute(index2) ? shift : 0;
}
}
/**
* Flips the order of the indexes.
*/
public void flipIndexes() {
int temp = index1;
index1 = index2;
index2 = temp;
}
/**
* The crossing method first computes the points on both lines (that contain the edges)
* who are closest in distance. If the distance between points is smaller than FastMath.FLT_EPSILON
* the we consider them to be the same point (the lines cross).
* The second step is to check if both points are contained within the edges.
*
* The method of computing the crossing point is as follows:
* Let's assume that:
* (P0, P1) are the points of the first edge
* (Q0, Q1) are the points of the second edge
*
* u = P1 - P0
* v = Q1 - Q0
*
* This gives us the equations of two lines:
* L1: (x = P1x + ux*t1; y = P1y + uy*t1; z = P1z + uz*t1)
* L2: (x = P2x + vx*t2; y = P2y + vy*t2; z = P2z + vz*t2)
*
* Comparing the x and y of the first two equations for each line will allow us to compute t1 and t2
* (which is implemented below).
* Using t1 and t2 we can compute (x, y, z) of each line and that will give us two points that we need to compare.
*
* @param edge
* the edge we check against crossing
* @return <b>true</b> if the edges cross and false otherwise
*/
public boolean cross(Edge edge) {
return this.getCrossPoint(edge) != null;
}
/**
* The method computes the crossing pint of this edge and another edge. If
* there is no crossing then null is returned.
*
* @param edge
* the edge to compute corss point with
* @return cross point on null if none exist
*/
public Vector3f getCrossPoint(Edge edge) {
return this.getCrossPoint(edge, false, false);
}
/**
* The method computes the crossing pint of this edge and another edge. If
* there is no crossing then null is returned. Also null is returned if the edges are parallel.
* This method also allows to get the crossing point of the straight lines that contain these edges if
* you set the 'extend' parameter to true.
*
* @param edge
* the edge to compute corss point with
* @param extendThisEdge
* set to <b>true</b> to find a crossing point along the whole
* straight that contains the current edge
* @param extendSecondEdge
* set to <b>true</b> to find a crossing point along the whole
* straight that contains the given edge
* @return cross point on null if none exist or the edges are parallel
*/
public Vector3f getCrossPoint(Edge edge, boolean extendThisEdge, boolean extendSecondEdge) {
Vector3d P1 = new Vector3d(this.getFirstVertex());
Vector3d P2 = new Vector3d(edge.getFirstVertex());
Vector3d u = new Vector3d(this.getSecondVertex()).subtract(P1).normalizeLocal();
Vector3d v = new Vector3d(edge.getSecondVertex()).subtract(P2).normalizeLocal();
if(Math.abs(u.dot(v)) >= 1 - FastMath.DBL_EPSILON) {
// the edges are parallel; do not care about the crossing point
return null;
}
double t1 = 0, t2 = 0;
if(u.x == 0 && v.x == 0) {
t2 = (u.z * (P2.y - P1.y) - u.y * (P2.z - P1.z)) / (u.y * v.z - u.z * v.y);
t1 = (P2.z - P1.z + v.z * t2) / u.z;
} else if(u.y == 0 && v.y == 0) {
t2 = (u.x * (P2.z - P1.z) - u.z * (P2.x - P1.x)) / (u.z * v.x - u.x * v.z);
t1 = (P2.x - P1.x + v.x * t2) / u.x;
} else if(u.z == 0 && v.z == 0) {
t2 = (u.x * (P2.y - P1.y) - u.y * (P2.x - P1.x)) / (u.y * v.x - u.x * v.y);
t1 = (P2.x - P1.x + v.x * t2) / u.x;
} else {
t2 = (P1.y * u.x - P1.x * u.y + P2.x * u.y - P2.y * u.x) / (v.y * u.x - u.y * v.x);
t1 = (P2.x - P1.x + v.x * t2) / u.x;
if(Math.abs(P1.z - P2.z + u.z * t1 - v.z * t2) > FastMath.FLT_EPSILON) {
return null;
}
}
Vector3d p1 = P1.add(u.mult(t1));
Vector3d p2 = P2.add(v.mult(t2));
if (p1.distance(p2) <= FastMath.FLT_EPSILON) {
if(extendThisEdge && extendSecondEdge) {
return p1.toVector3f();
}
// the lines cross, check if p1 and p2 are within the edges
Vector3d p = p1.subtract(P1);
double cos = p.dot(u) / p.length();
if (extendThisEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() - this.getLength() <= FastMath.FLT_EPSILON) {
// p1 is inside the first edge, lets check the other edge now
p = p2.subtract(P2);
cos = p.dot(v) / p.length();
if(extendSecondEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() - edge.getLength() <= FastMath.FLT_EPSILON) {
return p1.toVector3f();
}
}
}
return null;
}
@Override
public String toString() {
String result = "Edge [" + index1 + ", " + index2 + "] {" + crease + "}";
result += " (" + this.getFirstVertex() + " -> " + this.getSecondVertex() + ")";
if (inFace) {
result += "[F]";
}
return result;
}
@Override
public int hashCode() {
// The hash code must be identical for the same two indexes, no matter their order.
final int prime = 31;
int result = 1;
int lowerIndex = Math.min(index1, index2);
int higherIndex = Math.max(index1, index2);
result = prime * result + lowerIndex;
result = prime * result + higherIndex;
return result;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Edge)) {
return false;
}
if (this == obj) {
return true;
}
Edge other = (Edge) obj;
return Math.min(index1, index2) == Math.min(other.index1, other.index2) && Math.max(index1, index2) == Math.max(other.index1, other.index2);
}
/**
* The method loads all edges from the given mesh structure that does not belong to any face.
* @param meshStructure
* the mesh structure
* @param temporalMesh
* the owner of the edges
* @return all edges without faces
* @throws BlenderFileException
* an exception is thrown when problems with file reading occur
*/
public static List<Edge> loadAll(Structure meshStructure, TemporalMesh temporalMesh) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading all edges that do not belong to any face from mesh: {0}", meshStructure.getName());
List<Edge> result = new ArrayList<Edge>();
Pointer pMEdge = (Pointer) meshStructure.getFieldValue("medge");
if (pMEdge.isNotNull()) {
List<Structure> edges = pMEdge.fetchData();
for (Structure edge : edges) {
int flag = ((Number) edge.getFieldValue("flag")).intValue();
int v1 = ((Number) edge.getFieldValue("v1")).intValue();
int v2 = ((Number) edge.getFieldValue("v2")).intValue();
// I do not know why, but blender stores (possibly only sometimes) crease as negative values and shows positive in the editor
float crease = Math.abs(((Number) edge.getFieldValue("crease")).floatValue());
boolean edgeInFace = (flag & Edge.FLAG_EDGE_NOT_IN_FACE) == 0;
result.add(new Edge(v1, v2, crease, edgeInFace, temporalMesh));
}
}
LOGGER.log(Level.FINE, "Loaded {0} edges.", result.size());
return result;
}
}

@ -0,0 +1,613 @@
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
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.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A class that represents a single face in the mesh. The face is a polygon. Its minimum count of
* vertices is = 3.
*
* @author Marcin Roguski (Kaelthas)
*/
public class Face implements Comparator<Integer> {
private static final Logger LOGGER = Logger.getLogger(Face.class.getName());
/** The indexes loop of the face. */
private IndexesLoop indexes;
private List<IndexesLoop> triangulatedFaces;
/** Indicates if the face is smooth or solid. */
private boolean smooth;
/** The material index of the face. */
private int materialNumber;
/** UV coordinate sets attached to the face. The key is the set name and value are the UV coords. */
private Map<String, List<Vector2f>> faceUVCoords;
/** The vertex colors of the face. */
private List<byte[]> vertexColors;
/** The temporal mesh the face belongs to. */
private TemporalMesh temporalMesh;
/**
* Creates a complete face with all available data.
* @param indexes
* the indexes of the face (required)
* @param smooth
* indicates if the face is smooth or solid
* @param materialNumber
* the material index of the face
* @param faceUVCoords
* UV coordinate sets of the face (optional)
* @param vertexColors
* the vertex colors of the face (optional)
* @param temporalMesh
* the temporal mesh the face belongs to (required)
*/
public Face(Integer[] indexes, boolean smooth, int materialNumber, Map<String, List<Vector2f>> faceUVCoords, List<byte[]> vertexColors, TemporalMesh temporalMesh) {
this.setTemporalMesh(temporalMesh);
this.indexes = new IndexesLoop(indexes);
this.smooth = smooth;
this.materialNumber = materialNumber;
this.faceUVCoords = faceUVCoords;
this.temporalMesh = temporalMesh;
this.vertexColors = vertexColors;
}
/**
* Default constructor. Used by the clone method.
*/
private Face() {
}
@Override
public Face clone() {
Face result = new Face();
result.indexes = indexes.clone();
result.smooth = smooth;
result.materialNumber = materialNumber;
if (faceUVCoords != null) {
result.faceUVCoords = new HashMap<String, List<Vector2f>>(faceUVCoords.size());
for (Entry<String, List<Vector2f>> entry : faceUVCoords.entrySet()) {
List<Vector2f> uvs = new ArrayList<Vector2f>(entry.getValue().size());
for (Vector2f v : entry.getValue()) {
uvs.add(v.clone());
}
result.faceUVCoords.put(entry.getKey(), uvs);
}
}
if (vertexColors != null) {
result.vertexColors = new ArrayList<byte[]>(vertexColors.size());
for (byte[] colors : vertexColors) {
result.vertexColors.add(colors.clone());
}
}
result.temporalMesh = temporalMesh;
return result;
}
/**
* Returns the index at the given position in the index loop. If the given position is negative or exceeds
* the amount of vertices - it is being looped properly so that it always hits an index.
* For example getIndex(-1) will return the index before the 0 - in this case it will be the last one.
* @param indexPosition
* the index position
* @return index value at the given position
*/
private Integer getIndex(int indexPosition) {
if (indexPosition >= indexes.size()) {
indexPosition = indexPosition % indexes.size();
} else if (indexPosition < 0) {
indexPosition = indexes.size() - -indexPosition % indexes.size();
}
return indexes.get(indexPosition);
}
/**
* @return the mesh this face belongs to
*/
public TemporalMesh getTemporalMesh() {
return temporalMesh;
}
/**
* @return the original indexes of the face
*/
public IndexesLoop getIndexes() {
return indexes;
}
/**
* @return the centroid of the face
*/
public Vector3f computeCentroid() {
Vector3f result = new Vector3f();
List<Vector3f> vertices = temporalMesh.getVertices();
for (Integer index : indexes) {
result.addLocal(vertices.get(index));
}
return result.divideLocal(indexes.size());
}
/**
* @return current indexes of the face (if it is already triangulated then more than one index group will be in the result list)
*/
public List<List<Integer>> getCurrentIndexes() {
if (triangulatedFaces == null) {
return Arrays.asList(indexes.getAll());
}
List<List<Integer>> result = new ArrayList<List<Integer>>(triangulatedFaces.size());
for (IndexesLoop loop : triangulatedFaces) {
result.add(loop.getAll());
}
return result;
}
/**
* The method detaches the triangle from the face. This method keeps the indexes loop normalized - every index
* has only two neighbours. So if detaching the triangle causes a vertex to have more than two neighbours - it is
* also detached and returned as a result.
* The result is an empty list if no such situation happens.
* @param triangleIndexes
* the indexes of a triangle to be detached
* @return a list of faces that need to be detached as well in order to keep them normalized
* @throws BlenderFileException
* an exception is thrown when vertices of a face create more than one loop; this is found during path finding
*/
private List<Face> detachTriangle(Integer[] triangleIndexes) throws BlenderFileException {
LOGGER.fine("Detaching triangle.");
if (triangleIndexes.length != 3) {
throw new IllegalArgumentException("Cannot detach triangle with that does not have 3 indexes!");
}
MeshHelper meshHelper = temporalMesh.getBlenderContext().getHelper(MeshHelper.class);
List<Face> detachedFaces = new ArrayList<Face>();
List<Integer> path = new ArrayList<Integer>(indexes.size());
boolean[] edgeRemoved = new boolean[] { indexes.removeEdge(triangleIndexes[0], triangleIndexes[1]), indexes.removeEdge(triangleIndexes[0], triangleIndexes[2]), indexes.removeEdge(triangleIndexes[1], triangleIndexes[2]) };
Integer[][] indexesPairs = new Integer[][] { new Integer[] { triangleIndexes[0], triangleIndexes[1] }, new Integer[] { triangleIndexes[0], triangleIndexes[2] }, new Integer[] { triangleIndexes[1], triangleIndexes[2] } };
for (int i = 0; i < 3; ++i) {
if (!edgeRemoved[i]) {
indexes.findPath(indexesPairs[i][0], indexesPairs[i][1], path);
if (path.size() == 0) {
indexes.findPath(indexesPairs[i][1], indexesPairs[i][0], path);
}
if (path.size() == 0) {
throw new IllegalStateException("Triangulation failed. Cannot find path between two indexes. Please apply triangulation in Blender as a workaround.");
}
if (detachedFaces.size() == 0 && path.size() < indexes.size()) {
Integer[] indexesSublist = path.toArray(new Integer[path.size()]);
detachedFaces.add(new Face(indexesSublist, smooth, materialNumber, meshHelper.selectUVSubset(this, indexesSublist), meshHelper.selectVertexColorSubset(this, indexesSublist), temporalMesh));
for (int j = 0; j < path.size() - 1; ++j) {
indexes.removeEdge(path.get(j), path.get(j + 1));
}
indexes.removeEdge(path.get(path.size() - 1), path.get(0));
} else {
indexes.addEdge(path.get(path.size() - 1), path.get(0));
}
}
}
return detachedFaces;
}
/**
* Sets the temporal mesh for the face. The given mesh cannot be null.
* @param temporalMesh
* the temporal mesh of the face
* @throws IllegalArgumentException
* thrown if given temporal mesh is null
*/
public void setTemporalMesh(TemporalMesh temporalMesh) {
if (temporalMesh == null) {
throw new IllegalArgumentException("No temporal mesh for the face given!");
}
this.temporalMesh = temporalMesh;
}
/**
* Flips the order of the indexes.
*/
public void flipIndexes() {
indexes.reverse();
if (faceUVCoords != null) {
for (Entry<String, List<Vector2f>> entry : faceUVCoords.entrySet()) {
Collections.reverse(entry.getValue());
}
}
}
/**
* Flips UV coordinates.
* @param u
* indicates if U coords should be flipped
* @param v
* indicates if V coords should be flipped
*/
public void flipUV(boolean u, boolean v) {
if (faceUVCoords != null) {
for (Entry<String, List<Vector2f>> entry : faceUVCoords.entrySet()) {
for (Vector2f uv : entry.getValue()) {
uv.set(u ? 1 - uv.x : uv.x, v ? 1 - uv.y : uv.y);
}
}
}
}
/**
* @return the UV sets of the face
*/
public Map<String, List<Vector2f>> getUvSets() {
return faceUVCoords;
}
/**
* @return current vertex count of the face
*/
public int vertexCount() {
return indexes.size();
}
/**
* The method triangulates the face.
*/
public TriangulationWarning triangulate() {
LOGGER.fine("Triangulating face.");
assert indexes.size() >= 3 : "Invalid indexes amount for face. 3 is the required minimum!";
triangulatedFaces = new ArrayList<IndexesLoop>(indexes.size() - 2);
Integer[] indexes = new Integer[3];
TriangulationWarning warning = TriangulationWarning.NONE;
try {
List<Face> facesToTriangulate = new ArrayList<Face>(Arrays.asList(this.clone()));
while (facesToTriangulate.size() > 0 && warning == TriangulationWarning.NONE) {
Face face = facesToTriangulate.remove(0);
// two special cases will improve the computations speed
if(face.getIndexes().size() == 3) {
triangulatedFaces.add(face.getIndexes().clone());
} else {
int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
while (face.vertexCount() > 0) {
indexes[0] = face.getIndex(0);
indexes[1] = face.findClosestVertex(indexes[0], -1);
indexes[2] = face.findClosestVertex(indexes[0], indexes[1]);
LOGGER.finer("Veryfying improper triangulation of the temporal mesh.");
if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) {
warning = TriangulationWarning.CLOSEST_VERTS;
break;
}
if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) {
warning = TriangulationWarning.INFINITE_LOOP;
break;
}
previousIndex1 = indexes[0];
previousIndex2 = indexes[1];
previousIndex3 = indexes[2];
Arrays.sort(indexes, this);
facesToTriangulate.addAll(face.detachTriangle(indexes));
triangulatedFaces.add(new IndexesLoop(indexes));
}
}
}
} catch (BlenderFileException e) {
LOGGER.log(Level.WARNING, "Errors occurred during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage());
warning = TriangulationWarning.UNKNOWN;
}
if(warning != TriangulationWarning.NONE) {
LOGGER.finest("Triangulation the face using the most direct algorithm.");
indexes[0] = this.getIndex(0);
for (int i = 1; i < this.vertexCount() - 1; ++i) {
indexes[1] = this.getIndex(i);
indexes[2] = this.getIndex(i + 1);
triangulatedFaces.add(new IndexesLoop(indexes));
}
}
return warning;
}
/**
* A warning that indicates a problem with face triangulation. The warnings are collected and displayed once for each type for a mesh to
* avoid multiple warning loggings during triangulation. The amount of iterations can be really huge and logging every single failure would
* really slow down the importing process and make logs unreadable.
*
* @author Marcin Roguski (Kaelthas)
*/
public static enum TriangulationWarning {
NONE(null),
CLOSEST_VERTS("Unable to find two closest vertices while triangulating face."),
INFINITE_LOOP("Infinite loop detected during triangulation."),
UNKNOWN("There was an unknown problem with face triangulation. Please see log for details.");
private String description;
private TriangulationWarning(String description) {
this.description = description;
}
@Override
public String toString() {
return description;
}
}
/**
* @return <b>true</b> if the face is smooth and <b>false</b> otherwise
*/
public boolean isSmooth() {
return smooth;
}
/**
* @return the material index of the face
*/
public int getMaterialNumber() {
return materialNumber;
}
/**
* @return the vertices colord of the face
*/
public List<byte[]> getVertexColors() {
return vertexColors;
}
@Override
public String toString() {
return "Face " + indexes;
}
/**
* The method finds the closest vertex to the one specified by <b>index</b>.
* If the vertexToIgnore is positive than it will be ignored in the result.
* The closest vertex must be able to create an edge that is fully contained
* within the face and does not cross any other edges. Also if the
* vertexToIgnore is not negative then the condition that the edge between
* the found index and the one to ignore is inside the face must also be
* met.
*
* @param index
* the index of the vertex that needs to have found the nearest
* neighbour
* @param indexToIgnore
* the index to ignore in the result (pass -1 if none is to be
* ignored)
* @return the index of the closest vertex to the given one
*/
private int findClosestVertex(int index, int indexToIgnore) {
int result = -1;
List<Vector3f> vertices = temporalMesh.getVertices();
Vector3f v1 = vertices.get(index);
float distance = Float.MAX_VALUE;
for (int i : indexes) {
if (i != index && i != indexToIgnore) {
Vector3f v2 = vertices.get(i);
float d = v2.distance(v1);
if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh)) && (indexToIgnore < 0 || this.contains(new Edge(indexToIgnore, i, 0, true, temporalMesh)))) {
result = i;
distance = d;
}
}
}
return result;
}
/**
* The method verifies if the edge is contained within the face.
* It means it cannot cross any other edge and it must be inside the face and not outside of it.
* @param edge
* the edge to be checked
* @return <b>true</b> if the given edge is contained within the face and <b>false</b> otherwise
*/
private boolean contains(Edge edge) {
int index1 = edge.getFirstIndex();
int index2 = edge.getSecondIndex();
// check if the line between the vertices is not a border edge of the face
if (!indexes.areNeighbours(index1, index2)) {
for (int i = 0; i < indexes.size(); ++i) {
int i1 = this.getIndex(i - 1);
int i2 = this.getIndex(i);
// check if the edges have no common verts (because if they do, they cannot cross)
if (i1 != index1 && i1 != index2 && i2 != index1 && i2 != index2) {
if (edge.cross(new Edge(i1, i2, 0, false, temporalMesh))) {
return false;
}
}
}
// computing the edge's middle point
Vector3f edgeMiddlePoint = edge.computeCentroid();
// computing the edge that is perpendicular to the given edge and has a length of 1 (length actually does not matter)
Vector3f edgeVector = edge.getSecondVertex().subtract(edge.getFirstVertex());
Vector3f edgeNormal = temporalMesh.getNormals().get(index1).cross(edgeVector).normalizeLocal();
Edge e = new Edge(edgeMiddlePoint, edgeNormal.add(edgeMiddlePoint));
// compute the vectors from the middle point to the crossing between the extended edge 'e' and other edges of the face
List<Vector3f> crossingVectors = new ArrayList<Vector3f>();
for (int i = 0; i < indexes.size(); ++i) {
int i1 = this.getIndex(i);
int i2 = this.getIndex(i + 1);
Vector3f crossPoint = e.getCrossPoint(new Edge(i1, i2, 0, false, temporalMesh), true, false);
if(crossPoint != null) {
crossingVectors.add(crossPoint.subtractLocal(edgeMiddlePoint));
}
}
if(crossingVectors.size() == 0) {
return false;// edges do not cross
}
// use only distinct vertices (doubles may appear if the crossing point is a vertex)
List<Vector3f> distinctCrossingVectors = new ArrayList<Vector3f>();
for(Vector3f cv : crossingVectors) {
double minDistance = Double.MAX_VALUE;
for(Vector3f dcv : distinctCrossingVectors) {
minDistance = Math.min(minDistance, dcv.distance(cv));
}
if(minDistance > FastMath.FLT_EPSILON) {
distinctCrossingVectors.add(cv);
}
}
if(distinctCrossingVectors.size() == 0) {
throw new IllegalStateException("There MUST be at least 2 crossing vertices!");
}
// checking if all crossing vectors point to the same direction (if yes then the edge is outside the face)
float direction = Math.signum(distinctCrossingVectors.get(0).dot(edgeNormal));// if at least one vector has different direction that this - it means that the edge is inside the face
for(int i=1;i<distinctCrossingVectors.size();++i) {
if(direction != Math.signum(distinctCrossingVectors.get(i).dot(edgeNormal))) {
return true;
}
}
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + indexes.hashCode();
result = prime * result + temporalMesh.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Face)) {
return false;
}
Face other = (Face) obj;
if (!indexes.equals(other.indexes)) {
return false;
}
return temporalMesh.equals(other.temporalMesh);
}
/**
* Loads all faces of a given mesh.
* @param meshStructure
* the mesh structure we read the faces from
* @param userUVGroups
* UV groups defined by the user
* @param verticesColors
* the vertices colors of the mesh
* @param temporalMesh
* the temporal mesh the faces will belong to
* @param blenderContext
* the blender context
* @return list of faces read from the given mesh structure
* @throws BlenderFileException
* an exception is thrown when problems with file reading occur
*/
public static List<Face> loadAll(Structure meshStructure, Map<String, List<Vector2f>> userUVGroups, List<byte[]> verticesColors, TemporalMesh temporalMesh, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading all faces from mesh: {0}", meshStructure.getName());
List<Face> result = new ArrayList<Face>();
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
if (meshHelper.isBMeshCompatible(meshStructure)) {
LOGGER.fine("Reading BMesh.");
Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop");
Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly");
if (pMPoly.isNotNull() && pMLoop.isNotNull()) {
List<Structure> polys = pMPoly.fetchData();
List<Structure> loops = pMLoop.fetchData();
for (Structure poly : polys) {
int materialNumber = ((Number) poly.getFieldValue("mat_nr")).intValue();
int loopStart = ((Number) poly.getFieldValue("loopstart")).intValue();
int totLoop = ((Number) poly.getFieldValue("totloop")).intValue();
boolean smooth = (((Number) poly.getFieldValue("flag")).byteValue() & 0x01) != 0x00;
Integer[] vertexIndexes = new Integer[totLoop];
for (int i = loopStart; i < loopStart + totLoop; ++i) {
vertexIndexes[i - loopStart] = ((Number) loops.get(i).getFieldValue("v")).intValue();
}
// uvs always must be added wheater we have texture or not
Map<String, List<Vector2f>> uvCoords = new HashMap<String, List<Vector2f>>();
for (Entry<String, List<Vector2f>> entry : userUVGroups.entrySet()) {
List<Vector2f> uvs = entry.getValue().subList(loopStart, loopStart + totLoop);
uvCoords.put(entry.getKey(), new ArrayList<Vector2f>(uvs));
}
List<byte[]> vertexColors = null;
if (verticesColors != null && verticesColors.size() > 0) {
vertexColors = new ArrayList<byte[]>(totLoop);
for (int i = loopStart; i < loopStart + totLoop; ++i) {
vertexColors.add(verticesColors.get(i));
}
}
result.add(new Face(vertexIndexes, smooth, materialNumber, uvCoords, vertexColors, temporalMesh));
}
}
} else {
LOGGER.fine("Reading traditional faces.");
Pointer pMFace = (Pointer) meshStructure.getFieldValue("mface");
List<Structure> mFaces = pMFace.isNotNull() ? pMFace.fetchData() : null;
if (mFaces != null && mFaces.size() > 0) {
// indicates if the material with the specified number should have a texture attached
for (int i = 0; i < mFaces.size(); ++i) {
Structure mFace = mFaces.get(i);
int materialNumber = ((Number) mFace.getFieldValue("mat_nr")).intValue();
boolean smooth = (((Number) mFace.getFieldValue("flag")).byteValue() & 0x01) != 0x00;
int v1 = ((Number) mFace.getFieldValue("v1")).intValue();
int v2 = ((Number) mFace.getFieldValue("v2")).intValue();
int v3 = ((Number) mFace.getFieldValue("v3")).intValue();
int v4 = ((Number) mFace.getFieldValue("v4")).intValue();
int vertCount = v4 == 0 ? 3 : 4;
// uvs always must be added wheater we have texture or not
Map<String, List<Vector2f>> faceUVCoords = new HashMap<String, List<Vector2f>>();
for (Entry<String, List<Vector2f>> entry : userUVGroups.entrySet()) {
List<Vector2f> uvCoordsForASingleFace = new ArrayList<Vector2f>(vertCount);
for (int j = 0; j < vertCount; ++j) {
uvCoordsForASingleFace.add(entry.getValue().get(i * 4 + j));
}
faceUVCoords.put(entry.getKey(), uvCoordsForASingleFace);
}
List<byte[]> vertexColors = null;
if (verticesColors != null && verticesColors.size() > 0) {
vertexColors = new ArrayList<byte[]>(vertCount);
vertexColors.add(verticesColors.get(v1));
vertexColors.add(verticesColors.get(v2));
vertexColors.add(verticesColors.get(v3));
if (vertCount == 4) {
vertexColors.add(verticesColors.get(v4));
}
}
result.add(new Face(vertCount == 4 ? new Integer[] { v1, v2, v3, v4 } : new Integer[] { v1, v2, v3 }, smooth, materialNumber, faceUVCoords, vertexColors, temporalMesh));
}
}
}
LOGGER.log(Level.FINE, "Loaded {0} faces.", result.size());
return result;
}
@Override
public int compare(Integer index1, Integer index2) {
return indexes.indexOf(index1) - indexes.indexOf(index2);
}
}

@ -0,0 +1,305 @@
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
/**
* This class represents the Face's indexes loop. It is a simplified implementation of directed graph.
*
* @author Marcin Roguski (Kaelthas)
*/
public class IndexesLoop implements Comparator<Integer>, Iterable<Integer> {
public static final IndexPredicate INDEX_PREDICATE_USE_ALL = new IndexPredicate() {
@Override
public boolean execute(Integer index) {
return true;
}
};
/** The indexes. */
private List<Integer> nodes;
/** The edges of the indexes graph. The key is the 'from' index and 'value' is - 'to' index. */
private Map<Integer, List<Integer>> edges = new HashMap<Integer, List<Integer>>();
/**
* The constructor uses the given nodes in their give order. Each neighbour indexes will form an edge.
* @param nodes
* the nodes for the loop
*/
public IndexesLoop(Integer[] nodes) {
this.nodes = new ArrayList<Integer>(Arrays.asList(nodes));
this.prepareEdges(this.nodes);
}
@Override
public IndexesLoop clone() {
return new IndexesLoop(nodes.toArray(new Integer[nodes.size()]));
}
/**
* The method prepares edges for the given indexes.
* @param nodes
* the indexes
*/
private void prepareEdges(List<Integer> nodes) {
for (int i = 0; i < nodes.size() - 1; ++i) {
if (edges.containsKey(nodes.get(i))) {
edges.get(nodes.get(i)).add(nodes.get(i + 1));
} else {
edges.put(nodes.get(i), new ArrayList<Integer>(Arrays.asList(nodes.get(i + 1))));
}
}
edges.put(nodes.get(nodes.size() - 1), new ArrayList<Integer>(Arrays.asList(nodes.get(0))));
}
/**
* @return the count of indexes
*/
public int size() {
return nodes.size();
}
/**
* Adds edge to the loop.
* @param from
* the start index
* @param to
* the end index
*/
public void addEdge(Integer from, Integer to) {
if (nodes.contains(from) && nodes.contains(to)) {
if (edges.containsKey(from) && !edges.get(from).contains(to)) {
edges.get(from).add(to);
}
}
}
/**
* Removes edge from the face. The edge is removed if it already exists in the face.
* @param node1
* the first index of the edge to be removed
* @param node2
* the second index of the edge to be removed
* @return <b>true</b> if the edge was removed and <b>false</b> otherwise
*/
public boolean removeEdge(Integer node1, Integer node2) {
boolean edgeRemoved = false;
if (nodes.contains(node1) && nodes.contains(node2)) {
if (edges.containsKey(node1)) {
edgeRemoved |= edges.get(node1).remove(node2);
}
if (edges.containsKey(node2)) {
edgeRemoved |= edges.get(node2).remove(node1);
}
if (edgeRemoved) {
if (this.getNeighbourCount(node1) == 0) {
this.removeIndexes(node1);
}
if (this.getNeighbourCount(node2) == 0) {
this.removeIndexes(node2);
}
}
}
return edgeRemoved;
}
/**
* Tells if the given indexes are neighbours.
* @param index1
* the first index
* @param index2
* the second index
* @return <b>true</b> if the given indexes are neighbours and <b>false</b> otherwise
*/
public boolean areNeighbours(Integer index1, Integer index2) {
if (index1.equals(index2) || !edges.containsKey(index1) || !edges.containsKey(index2)) {
return false;
}
return edges.get(index1).contains(index2) || edges.get(index2).contains(index1);
}
/**
* Returns the value of the index located after the given one. Pointint the last index will return the first one.
* @param index
* the index value
* @return the value of 'next' index
*/
public Integer getNextIndex(Integer index) {
int i = nodes.indexOf(index);
return i == nodes.size() - 1 ? nodes.get(0) : nodes.get(i + 1);
}
/**
* Returns the value of the index located before the given one. Pointint the first index will return the last one.
* @param index
* the index value
* @return the value of 'previous' index
*/
public Integer getPreviousIndex(Integer index) {
int i = nodes.indexOf(index);
return i == 0 ? nodes.get(nodes.size() - 1) : nodes.get(i - 1);
}
/**
* The method shifts all indexes by a given value.
* @param shift
* the value to shift all indexes
* @param predicate
* the predicate that verifies which indexes should be shifted; if null then all will be shifted
*/
public void shiftIndexes(int shift, IndexPredicate predicate) {
if (predicate == null) {
predicate = INDEX_PREDICATE_USE_ALL;
}
List<Integer> nodes = new ArrayList<Integer>(this.nodes.size());
for (Integer node : this.nodes) {
nodes.add(node + (predicate.execute(node) ? shift : 0));
}
Map<Integer, List<Integer>> edges = new HashMap<Integer, List<Integer>>();
for (Entry<Integer, List<Integer>> entry : this.edges.entrySet()) {
List<Integer> neighbours = new ArrayList<Integer>(entry.getValue().size());
for (Integer neighbour : entry.getValue()) {
neighbours.add(neighbour + (predicate.execute(neighbour) ? shift : 0));
}
edges.put(entry.getKey() + shift, neighbours);
}
this.nodes = nodes;
this.edges = edges;
}
/**
* Reverses the order of the indexes.
*/
public void reverse() {
Collections.reverse(nodes);
edges.clear();
this.prepareEdges(nodes);
}
/**
* Returns the neighbour count of the given index.
* @param index
* the index whose neighbour count will be checked
* @return the count of neighbours of the given index
*/
private int getNeighbourCount(Integer index) {
int result = 0;
if (edges.containsKey(index)) {
result = edges.get(index).size();
for (List<Integer> neighbours : edges.values()) {
if (neighbours.contains(index)) {
++result;
}
}
}
return result;
}
/**
* Returns the position of the given index in the loop.
* @param index
* the index of the face
* @return the indexe's position in the loop
*/
public int indexOf(Integer index) {
return nodes.indexOf(index);
}
/**
* Returns the index at the given position.
* @param indexPosition
* the position of the index
* @return the index at a given position
*/
public Integer get(int indexPosition) {
return nodes.get(indexPosition);
}
/**
* @return all indexes of the face
*/
public List<Integer> getAll() {
return new ArrayList<Integer>(nodes);
}
/**
* The method removes all given indexes.
* @param indexes
* the indexes to be removed
*/
public void removeIndexes(Integer... indexes) {
for (Integer index : indexes) {
nodes.remove(index);
edges.remove(index);
for (List<Integer> neighbours : edges.values()) {
neighbours.remove(index);
}
}
}
/**
* The method finds the path between the given indexes.
* @param start
* the start index
* @param end
* the end index
* @param result
* a list containing indexes on the path from start to end (inclusive)
* @throws IllegalStateException
* an exception is thrown when the loop is not normalized (at least one
* index has more than 2 neighbours)
* @throws BlenderFileException
* an exception is thrown if the vertices of a face create more than one loop; this is thrown
* to prevent lack of memory errors during triangulation
*/
public void findPath(Integer start, Integer end, List<Integer> result) throws BlenderFileException {
result.clear();
Integer node = start;
while (!node.equals(end)) {
if (result.contains(node)) {
throw new BlenderFileException("Indexes of face have infinite loops!");
}
result.add(node);
List<Integer> nextSteps = edges.get(node);
if (nextSteps == null || nextSteps.size() == 0) {
result.clear();// no directed path from start to end
return;
} else if (nextSteps.size() == 1) {
node = nextSteps.get(0);
} else {
throw new BlenderFileException("Triangulation failed. Face has ambiguous indexes loop. Please triangulate your model in Blender as a workaround.");
}
}
result.add(end);
}
@Override
public String toString() {
return "IndexesLoop " + nodes.toString();
}
@Override
public int compare(Integer i1, Integer i2) {
return nodes.indexOf(i1) - nodes.indexOf(i2);
}
@Override
public Iterator<Integer> iterator() {
return nodes.iterator();
}
public static interface IndexPredicate {
boolean execute(Integer index);
}
}

@ -0,0 +1,364 @@
package com.jme3.scene.plugins.blender.meshes;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.util.BufferUtils;
/**
* A class that aggregates the mesh data to prepare proper buffers. The buffers refer only to ONE material.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class MeshBuffers {
private static final int MAXIMUM_WEIGHTS_PER_VERTEX = 4;
/** The material index. */
private final int materialIndex;
/** The vertices. */
private List<Vector3f> verts = new ArrayList<Vector3f>();
/** The normals. */
private List<Vector3f> normals = new ArrayList<Vector3f>();
/** The UV coordinate sets. */
private Map<String, List<Vector2f>> uvCoords = new HashMap<String, List<Vector2f>>();
/** The vertex colors. */
private List<byte[]> vertColors = new ArrayList<byte[]>();
/** The indexes. */
private List<Integer> indexes = new ArrayList<Integer>();
/** The maximum weights count assigned to a single vertex. Used during weights normalization. */
private int maximumWeightsPerVertex;
/** A list of mapping between weights and indexes. Each entry for the proper vertex. */
private List<TreeMap<Float, Integer>> boneWeightAndIndexes = new ArrayList<TreeMap<Float, Integer>>();
/**
* Constructor stores only the material index value.
* @param materialIndex
* the material index
*/
public MeshBuffers(int materialIndex) {
this.materialIndex = materialIndex;
}
/**
* @return the material index
*/
public int getMaterialIndex() {
return materialIndex;
}
/**
* @return indexes buffer
*/
public Buffer getIndexBuffer() {
if (indexes.size() <= Short.MAX_VALUE) {
short[] indices = new short[indexes.size()];
for (int i = 0; i < indexes.size(); ++i) {
indices[i] = indexes.get(i).shortValue();
}
return BufferUtils.createShortBuffer(indices);
} else {
int[] indices = new int[indexes.size()];
for (int i = 0; i < indexes.size(); ++i) {
indices[i] = indexes.get(i).intValue();
}
return BufferUtils.createIntBuffer(indices);
}
}
/**
* @return positions buffer
*/
public VertexBuffer getPositionsBuffer() {
VertexBuffer positionBuffer = new VertexBuffer(Type.Position);
Vector3f[] data = verts.toArray(new Vector3f[verts.size()]);
positionBuffer.setupData(Usage.Static, 3, Format.Float, BufferUtils.createFloatBuffer(data));
return positionBuffer;
}
/**
* @return normals buffer
*/
public VertexBuffer getNormalsBuffer() {
VertexBuffer positionBuffer = new VertexBuffer(Type.Normal);
Vector3f[] data = normals.toArray(new Vector3f[normals.size()]);
positionBuffer.setupData(Usage.Static, 3, Format.Float, BufferUtils.createFloatBuffer(data));
return positionBuffer;
}
/**
* @return bone buffers
*/
public BoneBuffersData getBoneBuffers() {
BoneBuffersData result = null;
if (maximumWeightsPerVertex > 0) {
this.normalizeBoneBuffers(MAXIMUM_WEIGHTS_PER_VERTEX);
maximumWeightsPerVertex = MAXIMUM_WEIGHTS_PER_VERTEX;
FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(boneWeightAndIndexes.size() * MAXIMUM_WEIGHTS_PER_VERTEX);
ByteBuffer indicesData = BufferUtils.createByteBuffer(boneWeightAndIndexes.size() * MAXIMUM_WEIGHTS_PER_VERTEX);
int index = 0;
for (Map<Float, Integer> boneBuffersData : boneWeightAndIndexes) {
if (boneBuffersData.size() > 0) {
int count = 0;
for (Entry<Float, Integer> entry : boneBuffersData.entrySet()) {
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getKey());
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getValue().byteValue());
++count;
}
} else {
// if no bone is assigned to this vertex then attach it to the 0-indexed root bone
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f);
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0);
}
++index;
}
VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight);
verticesWeights.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.Float, weightsFloatData);
VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex);
verticesWeightsIndices.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.UnsignedByte, indicesData);
result = new BoneBuffersData(maximumWeightsPerVertex, verticesWeights, verticesWeightsIndices);
}
return result;
}
/**
* @return UV coordinates sets
*/
public Map<String, List<Vector2f>> getUvCoords() {
return uvCoords;
}
/**
* @return <b>true</b> if vertex colors are used and <b>false</b> otherwise
*/
public boolean areVertexColorsUsed() {
return vertColors.size() > 0;
}
/**
* @return vertex colors buffer
*/
public ByteBuffer getVertexColorsBuffer() {
ByteBuffer result = null;
if (vertColors.size() > 0) {
result = BufferUtils.createByteBuffer(4 * vertColors.size());
for (byte[] v : vertColors) {
if (v != null) {
result.put(v[0]).put(v[1]).put(v[2]).put(v[3]);
} else {
result.put((byte) 0).put((byte) 0).put((byte) 0).put((byte) 0);
}
}
result.flip();
}
return result;
}
/**
* @return <b>true</b> if indexes can be shorts' and <b>false</b> if they need to be ints'
*/
public boolean isShortIndexBuffer() {
return indexes.size() <= Short.MAX_VALUE;
}
/**
* Appends a vertex and normal to the buffers.
* @param vert
* vertex
* @param normal
* normal vector
*/
public void append(Vector3f vert, Vector3f normal) {
int index = this.indexOf(vert, normal, null);
if (index >= 0) {
indexes.add(index);
} else {
indexes.add(verts.size());
verts.add(vert);
normals.add(normal);
}
}
/**
* Appends the face data to the buffers.
* @param smooth
* tells if the face is smooth or flat
* @param verts
* the vertices
* @param normals
* the normals
* @param uvCoords
* the UV coordinates
* @param vertColors
* the vertex colors
* @param vertexGroups
* the vertex groups
*/
public void append(boolean smooth, Vector3f[] verts, Vector3f[] normals, Map<String, List<Vector2f>> uvCoords, byte[][] vertColors, List<Map<Float, Integer>> vertexGroups) {
if (verts.length != normals.length) {
throw new IllegalArgumentException("The amount of verts and normals MUST be equal!");
}
if (vertColors != null && vertColors.length != verts.length) {
throw new IllegalArgumentException("The amount of vertex colors and vertices MUST be equal!");
}
if (vertexGroups.size() != 0 && vertexGroups.size() != verts.length) {
throw new IllegalArgumentException("The amount of (if given) vertex groups and vertices MUST be equal!");
}
if (!smooth) {
// make the normals perpendicular to the face
normals[0] = normals[1] = normals[2] = FastMath.computeNormal(verts[0], verts[1], verts[2]);
}
for (int i = 0; i < verts.length; ++i) {
int index = -1;
Map<String, Vector2f> uvCoordsForVertex = this.getUVsForVertex(i, uvCoords);
if (smooth && (index = this.indexOf(verts[i], normals[i], uvCoordsForVertex)) >= 0) {
indexes.add(index);
} else {
indexes.add(this.verts.size());
this.verts.add(verts[i]);
this.normals.add(normals[i]);
this.vertColors.add(vertColors[i]);
if (uvCoords != null && uvCoords.size() > 0) {
for (Entry<String, List<Vector2f>> entry : uvCoords.entrySet()) {
if (this.uvCoords.containsKey(entry.getKey())) {
this.uvCoords.get(entry.getKey()).add(entry.getValue().get(i));
} else {
List<Vector2f> uvs = new ArrayList<Vector2f>();
uvs.add(entry.getValue().get(i));
this.uvCoords.put(entry.getKey(), uvs);
}
}
}
if (vertexGroups != null && vertexGroups.size() > 0) {
Map<Float, Integer> group = vertexGroups.get(i);
maximumWeightsPerVertex = Math.max(maximumWeightsPerVertex, group.size());
boneWeightAndIndexes.add(new TreeMap<Float, Integer>(group));
}
}
}
}
/**
* Returns UV coordinates assigned for the vertex with the proper index.
* @param vertexIndex
* the index of the vertex
* @param uvs
* all UV coordinates we search in
* @return a set of UV coordinates assigned to the given vertex
*/
private Map<String, Vector2f> getUVsForVertex(int vertexIndex, Map<String, List<Vector2f>> uvs) {
if (uvs == null || uvs.size() == 0) {
return null;
}
Map<String, Vector2f> result = new HashMap<String, Vector2f>(uvs.size());
for (Entry<String, List<Vector2f>> entry : uvs.entrySet()) {
result.put(entry.getKey(), entry.getValue().get(vertexIndex));
}
return result;
}
/**
* The method returns an index of a vertex described by the given data.
* The method tries to find a vertex that mathes the given data. If it does it means
* that such vertex is already used.
* @param vert
* the vertex position coordinates
* @param normal
* the vertex's normal vector
* @param uvCoords
* the UV coords of the vertex
* @return index of the found vertex of -1
*/
private int indexOf(Vector3f vert, Vector3f normal, Map<String, Vector2f> uvCoords) {
for (int i = 0; i < verts.size(); ++i) {
if (verts.get(i).equals(vert) && normals.get(i).equals(normal)) {
if (uvCoords != null && uvCoords.size() > 0) {
for (Entry<String, Vector2f> entry : uvCoords.entrySet()) {
List<Vector2f> uvs = this.uvCoords.get(entry.getKey());
if (uvs == null) {
return -1;
}
if (!uvs.get(i).equals(entry.getValue())) {
return -1;
}
}
}
return i;
}
}
return -1;
}
/**
* The method normalizes the weights and bone indexes data.
* First it truncates the amount to MAXIMUM_WEIGHTS_PER_VERTEX because this is how many weights JME can handle.
* Next it normalizes the weights so that the sum of all verts is 1.
* @param maximumSize
* the maximum size that the data will be truncated to (usually: MAXIMUM_WEIGHTS_PER_VERTEX)
*/
private void normalizeBoneBuffers(int maximumSize) {
for (TreeMap<Float, Integer> group : boneWeightAndIndexes) {
if (group.size() > maximumSize) {
NavigableMap<Float, Integer> descendingWeights = group.descendingMap();
while (descendingWeights.size() > maximumSize) {
descendingWeights.pollLastEntry();
}
}
// normalizing the weights so that the sum of the values is equal to '1'
TreeMap<Float, Integer> normalizedGroup = new TreeMap<Float, Integer>();
float sum = 0;
for (Entry<Float, Integer> entry : group.entrySet()) {
sum += entry.getKey();
}
if (sum != 0 && sum != 1) {
for (Entry<Float, Integer> entry : group.entrySet()) {
normalizedGroup.put(entry.getKey() / sum, entry.getValue());
}
group.clear();
group.putAll(normalizedGroup);
}
}
}
/**
* A class that gathers the data for mesh bone buffers.
* Added to increase code readability.
*
* @author Marcin Roguski (Kaelthas)
*/
public static class BoneBuffersData {
public final int maximumWeightsPerVertex;
public final VertexBuffer verticesWeights;
public final VertexBuffer verticesWeightsIndices;
public BoneBuffersData(int maximumWeightsPerVertex, VertexBuffer verticesWeights, VertexBuffer verticesWeightsIndices) {
this.maximumWeightsPerVertex = maximumWeightsPerVertex;
this.verticesWeights = verticesWeights;
this.verticesWeightsIndices = verticesWeightsIndices;
}
}
}

@ -0,0 +1,381 @@
/*
* Copyright (c) 2009-2019 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.objects.Properties;
/**
* A class that is used in mesh calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class MeshHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(MeshHelper.class.getName());
/** A type of UV data layer in traditional faced mesh (triangles or quads). */
public static final int UV_DATA_LAYER_TYPE_FMESH = 5;
/** A type of UV data layer in bmesh type. */
public static final int UV_DATA_LAYER_TYPE_BMESH = 16;
/** A material used for single lines and points. */
private Material blackUnshadedMaterial;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
* versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public MeshHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* Converts the mesh structure into temporal mesh.
* The temporal mesh is stored in blender context and here always a clone is being returned because the mesh might
* be modified by modifiers.
*
* @param meshStructure
* the mesh structure
* @param blenderContext
* the blender context
* @return temporal mesh read from the given structure
* @throws BlenderFileException
* an exception is thrown when problems with reading blend file occur
*/
public TemporalMesh toTemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading temporal mesh named: {0}.", meshStructure.getName());
TemporalMesh temporalMesh = (TemporalMesh) blenderContext.getLoadedFeature(meshStructure.getOldMemoryAddress(), LoadedDataType.TEMPORAL_MESH);
if (temporalMesh != null) {
LOGGER.fine("The mesh is already loaded. Returning its 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();
LOGGER.log(Level.FINE, "Reading mesh: {0}.", name);
temporalMesh = new TemporalMesh(meshStructure, blenderContext);
LOGGER.fine("Loading materials.");
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
temporalMesh.setMaterials(materialHelper.getMaterials(meshStructure, blenderContext));
LOGGER.fine("Reading custom properties.");
Properties properties = this.loadProperties(meshStructure, blenderContext);
temporalMesh.setProperties(properties);
blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, meshStructure);
blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.TEMPORAL_MESH, temporalMesh);
return temporalMesh.clone();
}
/**
* Tells if the given mesh structure supports BMesh.
*
* @param meshStructure
* the mesh structure
* @return <b>true</b> if BMesh is supported and <b>false</b> otherwise
*/
public boolean isBMeshCompatible(Structure meshStructure) {
Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop");
Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly");
return pMLoop != null && pMPoly != null && pMLoop.isNotNull() && pMPoly.isNotNull();
}
/**
* This method returns the vertices: a list of vertex positions and a list
* of vertex normals.
*
* @param meshStructure
* the structure containing the mesh data
* @throws BlenderFileException
* this exception is thrown when the blend file structure is somehow invalid or corrupted
*/
@SuppressWarnings("unchecked")
public void loadVerticesAndNormals(Structure meshStructure, List<Vector3f> vertices, List<Vector3f> normals) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading vertices and normals from mesh: {0}.", meshStructure.getName());
int count = ((Number) meshStructure.getFieldValue("totvert")).intValue();
if (count > 0) {
Pointer pMVert = (Pointer) meshStructure.getFieldValue("mvert");
List<Structure> mVerts = pMVert.fetchData();
Vector3f co = null, no = null;
if (fixUpAxis) {
for (int i = 0; i < count; ++i) {
DynamicArray<Number> coordinates = (DynamicArray<Number>) mVerts.get(i).getFieldValue("co");
co = new Vector3f(coordinates.get(0).floatValue(), coordinates.get(2).floatValue(), -coordinates.get(1).floatValue());
vertices.add(co);
DynamicArray<Number> norm = (DynamicArray<Number>) mVerts.get(i).getFieldValue("no");
no = new Vector3f(norm.get(0).shortValue() / 32767.0f, norm.get(2).shortValue() / 32767.0f, -norm.get(1).shortValue() / 32767.0f);
normals.add(no);
}
} else {
for (int i = 0; i < count; ++i) {
DynamicArray<Number> coordinates = (DynamicArray<Number>) mVerts.get(i).getFieldValue("co");
co = new Vector3f(coordinates.get(0).floatValue(), coordinates.get(1).floatValue(), coordinates.get(2).floatValue());
vertices.add(co);
DynamicArray<Number> norm = (DynamicArray<Number>) mVerts.get(i).getFieldValue("no");
no = new Vector3f(norm.get(0).shortValue() / 32767.0f, norm.get(1).shortValue() / 32767.0f, norm.get(2).shortValue() / 32767.0f);
normals.add(no);
}
}
}
LOGGER.log(Level.FINE, "Loaded {0} vertices and normals.", vertices.size());
}
/**
* This method returns the vertices colors. Each vertex is stored in byte[4] array.
*
* @param meshStructure
* the structure containing the mesh data
* @param blenderContext
* the blender context
* @return a list of vertices colors, each color belongs to a single vertex or empty list of colors are not specified
* @throws BlenderFileException
* this exception is thrown when the blend file structure is somehow invalid or corrupted
*/
public List<byte[]> loadVerticesColors(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading vertices colors from mesh: {0}.", meshStructure.getName());
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
Pointer pMCol = (Pointer) meshStructure.getFieldValue(meshHelper.isBMeshCompatible(meshStructure) ? "mloopcol" : "mcol");
List<byte[]> verticesColors = new ArrayList<byte[]>();
// it was likely a bug in blender untill version 2.63 (the blue and red factors were misplaced in their structure)
// so we need to put them right
boolean useBGRA = blenderContext.getBlenderVersion() < 263;
if (pMCol.isNotNull()) {
List<Structure> mCol = pMCol.fetchData();
for (Structure color : mCol) {
byte r = ((Number) color.getFieldValue("r")).byteValue();
byte g = ((Number) color.getFieldValue("g")).byteValue();
byte b = ((Number) color.getFieldValue("b")).byteValue();
byte a = ((Number) color.getFieldValue("a")).byteValue();
verticesColors.add(useBGRA ? new byte[] { b, g, r, a } : new byte[] { r, g, b, a });
}
}
return verticesColors;
}
/**
* The method loads the UV coordinates. The result is a map where the key is the user's UV set name and the values are UV coordinates.
* But depending on the mesh type (triangle/quads or bmesh) the lists in the map have different meaning.
* For bmesh they are enlisted just like they are stored in the blend file (in loops).
* For traditional faces every 4 UV's should be assigned for a single face.
* @param meshStructure
* the mesh structure
* @return a map that sorts UV coordinates between different UV sets
* @throws BlenderFileException
* an exception is thrown when problems with blend file occur
*/
@SuppressWarnings("unchecked")
public LinkedHashMap<String, List<Vector2f>> loadUVCoordinates(Structure meshStructure) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading UV coordinates from mesh: {0}.", meshStructure.getName());
LinkedHashMap<String, List<Vector2f>> result = new LinkedHashMap<String, List<Vector2f>>();
if (this.isBMeshCompatible(meshStructure)) {
// in this case the UV's are assigned to vertices (an array is the same length as the vertex array)
Structure loopData = (Structure) meshStructure.getFieldValue("ldata");
Pointer pLoopDataLayers = (Pointer) loopData.getFieldValue("layers");
List<Structure> loopDataLayers = pLoopDataLayers.fetchData();
for (Structure structure : loopDataLayers) {
Pointer p = (Pointer) structure.getFieldValue("data");
if (p.isNotNull() && ((Number) structure.getFieldValue("type")).intValue() == MeshHelper.UV_DATA_LAYER_TYPE_BMESH) {
String uvSetName = structure.getFieldValue("name").toString();
List<Structure> uvsStructures = p.fetchData();
List<Vector2f> uvs = new ArrayList<Vector2f>(uvsStructures.size());
for (Structure uvStructure : uvsStructures) {
DynamicArray<Number> loopUVS = (DynamicArray<Number>) uvStructure.getFieldValue("uv");
uvs.add(new Vector2f(loopUVS.get(0).floatValue(), loopUVS.get(1).floatValue()));
}
result.put(uvSetName, uvs);
}
}
} else {
// in this case UV's are assigned to faces (the array has the same length as the faces count)
Structure facesData = (Structure) meshStructure.getFieldValue("fdata");
Pointer pFacesDataLayers = (Pointer) facesData.getFieldValue("layers");
if (pFacesDataLayers.isNotNull()) {
List<Structure> facesDataLayers = pFacesDataLayers.fetchData();
for (Structure structure : facesDataLayers) {
Pointer p = (Pointer) structure.getFieldValue("data");
if (p.isNotNull() && ((Number) structure.getFieldValue("type")).intValue() == MeshHelper.UV_DATA_LAYER_TYPE_FMESH) {
String uvSetName = structure.getFieldValue("name").toString();
List<Structure> uvsStructures = p.fetchData();
List<Vector2f> uvs = new ArrayList<Vector2f>(uvsStructures.size());
for (Structure uvStructure : uvsStructures) {
DynamicArray<Number> mFaceUVs = (DynamicArray<Number>) uvStructure.getFieldValue("uv");
uvs.add(new Vector2f(mFaceUVs.get(0).floatValue(), mFaceUVs.get(1).floatValue()));
uvs.add(new Vector2f(mFaceUVs.get(2).floatValue(), mFaceUVs.get(3).floatValue()));
uvs.add(new Vector2f(mFaceUVs.get(4).floatValue(), mFaceUVs.get(5).floatValue()));
uvs.add(new Vector2f(mFaceUVs.get(6).floatValue(), mFaceUVs.get(7).floatValue()));
}
result.put(uvSetName, uvs);
}
}
}
}
return result;
}
/**
* Loads all vertices groups.
* @param meshStructure
* the mesh structure
* @return a list of vertex groups for every vertex in the mesh
* @throws BlenderFileException
* an exception is thrown when problems with blend file occur
*/
public List<Map<String, Float>> loadVerticesGroups(Structure meshStructure) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading vertices groups from mesh: {0}.", meshStructure.getName());
List<Map<String, Float>> result = new ArrayList<Map<String, Float>>();
Structure parent = blenderContext.peekParent();
if(parent != null) {
// the mesh might be saved without its parent (it is then unused)
Structure defbase = (Structure) parent.getFieldValue("defbase");
List<String> groupNames = new ArrayList<String>();
List<Structure> defs = defbase.evaluateListBase();
if(!defs.isEmpty()) {
for (Structure def : defs) {
groupNames.add(def.getFieldValue("name").toString());
}
Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
if (pDvert.isNotNull()) {// assigning weights and bone indices
List<Structure> dverts = pDvert.fetchData();
for (Structure dvert : dverts) {
Map<String, Float> weightsForVertex = new HashMap<String, Float>();
Pointer pDW = (Pointer) dvert.getFieldValue("dw");
if (pDW.isNotNull()) {
List<Structure> dw = pDW.fetchData();
for (Structure deformWeight : dw) {
int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue();
float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
String groupName = groupNames.get(groupIndex);
weightsForVertex.put(groupName, weight);
}
}
result.add(weightsForVertex);
}
}
}
}
return result;
}
/**
* Selects the proper subsets of UV coordinates for the given sublist of indexes.
* @param face
* the face with the original UV sets
* @param indexesSublist
* the sub list of indexes
* @return a map of UV coordinates subsets
*/
public Map<String, List<Vector2f>> selectUVSubset(Face face, Integer... indexesSublist) {
Map<String, List<Vector2f>> result = null;
if (face.getUvSets() != null) {
result = new HashMap<String, List<Vector2f>>();
for (Entry<String, List<Vector2f>> entry : face.getUvSets().entrySet()) {
List<Vector2f> uvs = new ArrayList<Vector2f>(indexesSublist.length);
for (Integer index : indexesSublist) {
uvs.add(entry.getValue().get(face.getIndexes().indexOf(index)));
}
result.put(entry.getKey(), uvs);
}
}
return result;
}
/**
* Selects the proper subsets of vertex colors for the given sublist of indexes.
* @param face
* the face with the original vertex colors
* @param indexesSublist
* the sub list of indexes
* @return a sublist of vertex colors
*/
public List<byte[]> selectVertexColorSubset(Face face, Integer... indexesSublist) {
List<byte[]> result = null;
List<byte[]> vertexColors = face.getVertexColors();
if (vertexColors != null) {
result = new ArrayList<byte[]>(indexesSublist.length);
for (Integer index : indexesSublist) {
result.add(vertexColors.get(face.getIndexes().indexOf(index)));
}
}
return result;
}
/**
* Returns the black unshaded material. It is used for lines and points because that is how blender
* renders it.
* @param blenderContext
* the blender context
* @return black unshaded material
*/
public synchronized Material getBlackUnshadedMaterial(BlenderContext blenderContext) {
if (blackUnshadedMaterial == null) {
blackUnshadedMaterial = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
blackUnshadedMaterial.setColor("Color", ColorRGBA.Black);
}
return blackUnshadedMaterial;
}
}

@ -0,0 +1,93 @@
package com.jme3.scene.plugins.blender.meshes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate;
/**
* A class that represents a single point on the scene that is not a part of an edge.
*
* @author Marcin Roguski (Kaelthas)
*/
public class Point {
private static final Logger LOGGER = Logger.getLogger(Point.class.getName());
/** The point's index. */
private int index;
/**
* Constructs a point for a given index.
* @param index
* the index of the point
*/
public Point(int index) {
this.index = index;
}
@Override
public Point clone() {
return new Point(index);
}
/**
* @return the index of the point
*/
public int getIndex() {
return index;
}
/**
* The method shifts the index by a given value.
* @param shift
* the value to shift the index
* @param predicate
* the predicate that verifies which indexes should be shifted; if null then all will be shifted
*/
public void shiftIndexes(int shift, IndexPredicate predicate) {
if (predicate == null || predicate.execute(index)) {
index += shift;
}
}
/**
* Loads all points of the mesh that do not belong to any edge.
* @param meshStructure
* the mesh structure
* @return a list of points
* @throws BlenderFileException
* an exception is thrown when problems with file reading occur
*/
public static List<Point> loadAll(Structure meshStructure) throws BlenderFileException {
LOGGER.log(Level.FINE, "Loading all points that do not belong to any edge from mesh: {0}", meshStructure.getName());
List<Point> result = new ArrayList<Point>();
Pointer pMEdge = (Pointer) meshStructure.getFieldValue("medge");
if (pMEdge.isNotNull()) {
int count = ((Number) meshStructure.getFieldValue("totvert")).intValue();
Set<Integer> unusedVertices = new HashSet<Integer>(count);
for (int i = 0; i < count; ++i) {
unusedVertices.add(i);
}
List<Structure> edges = pMEdge.fetchData();
for (Structure edge : edges) {
unusedVertices.remove(((Number) edge.getFieldValue("v1")).intValue());
unusedVertices.remove(((Number) edge.getFieldValue("v2")).intValue());
}
for (Integer unusedIndex : unusedVertices) {
result.add(new Point(unusedIndex));
}
}
LOGGER.log(Level.FINE, "Loaded {0} points.", result.size());
return result;
}
}

@ -0,0 +1,767 @@
package com.jme3.scene.plugins.blender.meshes;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingVolume;
import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.meshes.Face.TriangulationWarning;
import com.jme3.scene.plugins.blender.meshes.MeshBuffers.BoneBuffersData;
import com.jme3.scene.plugins.blender.modifiers.Modifier;
import com.jme3.scene.plugins.blender.objects.Properties;
/**
* The class extends Geometry so that it can be temporalily added to the object's node.
* Later each such node's child will be transformed into a list of geometries.
*
* @author Marcin Roguski (Kaelthas)
*/
public class TemporalMesh extends Geometry {
private static final Logger LOGGER = Logger.getLogger(TemporalMesh.class.getName());
/** A minimum weight value. */
private static final double MINIMUM_BONE_WEIGHT = FastMath.DBL_EPSILON;
/** The blender context. */
protected final BlenderContext blenderContext;
/** The mesh's structure. */
protected final Structure meshStructure;
/** Loaded vertices. */
protected List<Vector3f> vertices = new ArrayList<Vector3f>();
/** Loaded normals. */
protected List<Vector3f> normals = new ArrayList<Vector3f>();
/** Loaded vertex groups. */
protected List<Map<String, Float>> vertexGroups = new ArrayList<Map<String, Float>>();
/** Loaded vertex colors. */
protected List<byte[]> verticesColors = new ArrayList<byte[]>();
/** Materials used by the mesh. */
protected MaterialContext[] materials;
/** The properties of the mesh. */
protected Properties properties;
/** The bone indexes. */
protected Map<String, Integer> boneIndexes = new HashMap<String, Integer>();
/** The modifiers that should be applied after the mesh has been created. */
protected List<Modifier> postMeshCreationModifiers = new ArrayList<Modifier>();
/** The faces of the mesh. */
protected List<Face> faces = new ArrayList<Face>();
/** The edges of the mesh. */
protected List<Edge> edges = new ArrayList<Edge>();
/** The points of the mesh. */
protected List<Point> points = new ArrayList<Point>();
/** A map between index and faces that contain it (for faster index - face queries). */
protected Map<Integer, Set<Face>> indexToFaceMapping = new HashMap<Integer, Set<Face>>();
/** A map between index and edges that contain it (for faster index - edge queries). */
protected Map<Integer, Set<Edge>> indexToEdgeMapping = new HashMap<Integer, Set<Edge>>();
/** The bounding box of the temporal mesh. */
protected BoundingBox boundingBox;
/**
* Creates a temporal mesh based on the given mesh structure.
* @param meshStructure
* the mesh structure
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when problems with file reading occur
*/
public TemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException {
this(meshStructure, blenderContext, true);
}
/**
* Creates a temporal mesh based on the given mesh structure.
* @param meshStructure
* the mesh structure
* @param blenderContext
* the blender context
* @param loadData
* tells if the data should be loaded from the mesh structure
* @throws BlenderFileException
* an exception is thrown when problems with file reading occur
*/
protected TemporalMesh(Structure meshStructure, BlenderContext blenderContext, boolean loadData) throws BlenderFileException {
this.blenderContext = blenderContext;
this.meshStructure = meshStructure;
if (loadData) {
name = meshStructure.getName();
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
meshHelper.loadVerticesAndNormals(meshStructure, vertices, normals);
verticesColors = meshHelper.loadVerticesColors(meshStructure, blenderContext);
LinkedHashMap<String, List<Vector2f>> userUVGroups = meshHelper.loadUVCoordinates(meshStructure);
vertexGroups = meshHelper.loadVerticesGroups(meshStructure);
faces = Face.loadAll(meshStructure, userUVGroups, verticesColors, this, blenderContext);
edges = Edge.loadAll(meshStructure, this);
points = Point.loadAll(meshStructure);
this.rebuildIndexesMappings();
}
}
/**
* @return the blender context
*/
public BlenderContext getBlenderContext() {
return blenderContext;
}
/**
* @return the vertices of the mesh
*/
public List<Vector3f> getVertices() {
return vertices;
}
/**
* @return the normals of the mesh
*/
public List<Vector3f> getNormals() {
return normals;
}
/**
* @return all faces
*/
public List<Face> getFaces() {
return faces;
}
/**
* @return all edges
*/
public List<Edge> getEdges() {
return edges;
}
/**
* @return all points (do not mistake it with vertices)
*/
public List<Point> getPoints() {
return points;
}
/**
* @return all vertices colors
*/
public List<byte[]> getVerticesColors() {
return verticesColors;
}
/**
* @return all vertex groups for the vertices (each map has groups for the proper vertex)
*/
public List<Map<String, Float>> getVertexGroups() {
return vertexGroups;
}
/**
* @return the faces that contain the given index or null if none contain it
*/
public Collection<Face> getAdjacentFaces(Integer index) {
return indexToFaceMapping.get(index);
}
/**
* @param edge the edge of the mesh
* @return a list of faces that contain the given edge or an empty list
*/
public Collection<Face> getAdjacentFaces(Edge edge) {
List<Face> result = new ArrayList<Face>(indexToFaceMapping.get(edge.getFirstIndex()));
Set<Face> secondIndexAdjacentFaces = indexToFaceMapping.get(edge.getSecondIndex());
if (secondIndexAdjacentFaces != null) {
result.retainAll(indexToFaceMapping.get(edge.getSecondIndex()));
}
return result;
}
/**
* @param index the index of the mesh
* @return a list of edges that contain the index
*/
public Collection<Edge> getAdjacentEdges(Integer index) {
return indexToEdgeMapping.get(index);
}
/**
* Tells if the given edge is a boundary edge. The boundary edge means that it belongs to a single
* face or to none.
* @param edge the edge of the mesh
* @return <b>true</b> if the edge is a boundary one and <b>false</b> otherwise
*/
public boolean isBoundary(Edge edge) {
return this.getAdjacentFaces(edge).size() <= 1;
}
/**
* The method tells if the given index is a boundary index. A boundary index belongs to at least
* one boundary edge.
* @param index
* the index of the mesh
* @return <b>true</b> if the index is a boundary one and <b>false</b> otherwise
*/
public boolean isBoundary(Integer index) {
Collection<Edge> adjacentEdges = this.getAdjacentEdges(index);
for (Edge edge : adjacentEdges) {
if (this.isBoundary(edge)) {
return true;
}
}
return false;
}
@Override
public TemporalMesh clone() {
try {
TemporalMesh result = new TemporalMesh(meshStructure, blenderContext, false);
result.name = name;
for (Vector3f v : vertices) {
result.vertices.add(v.clone());
}
for (Vector3f n : normals) {
result.normals.add(n.clone());
}
for (Map<String, Float> group : vertexGroups) {
result.vertexGroups.add(new HashMap<String, Float>(group));
}
for (byte[] vertColor : verticesColors) {
result.verticesColors.add(vertColor.clone());
}
result.materials = materials;
result.properties = properties;
result.boneIndexes.putAll(boneIndexes);
result.postMeshCreationModifiers.addAll(postMeshCreationModifiers);
for (Face face : faces) {
result.faces.add(face.clone());
}
for (Edge edge : edges) {
result.edges.add(edge.clone());
}
for (Point point : points) {
result.points.add(point.clone());
}
result.rebuildIndexesMappings();
return result;
} catch (BlenderFileException e) {
LOGGER.log(Level.SEVERE, "Error while cloning the temporal mesh: {0}. Returning null.", e.getLocalizedMessage());
}
return null;
}
/**
* The method rebuilds the mappings between faces and edges. Should be called after
* every major change of the temporal mesh done outside it.
* <p>
* Note: I will remove this method soon and cause the mappings to be done
* automatically when the mesh is modified.
*/
public void rebuildIndexesMappings() {
indexToEdgeMapping.clear();
indexToFaceMapping.clear();
for (Face face : faces) {
for (Integer index : face.getIndexes()) {
Set<Face> faces = indexToFaceMapping.get(index);
if (faces == null) {
faces = new HashSet<Face>();
indexToFaceMapping.put(index, faces);
}
faces.add(face);
}
}
for (Edge edge : edges) {
Set<Edge> edges = indexToEdgeMapping.get(edge.getFirstIndex());
if (edges == null) {
edges = new HashSet<Edge>();
indexToEdgeMapping.put(edge.getFirstIndex(), edges);
}
edges.add(edge);
edges = indexToEdgeMapping.get(edge.getSecondIndex());
if (edges == null) {
edges = new HashSet<Edge>();
indexToEdgeMapping.put(edge.getSecondIndex(), edges);
}
edges.add(edge);
}
}
@Override
public void updateModelBound() {
if (boundingBox == null) {
boundingBox = new BoundingBox();
}
Vector3f min = new Vector3f(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
Vector3f max = new Vector3f(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
for (Vector3f v : vertices) {
min.set(Math.min(min.x, v.x), Math.min(min.y, v.y), Math.min(min.z, v.z));
max.set(Math.max(max.x, v.x), Math.max(max.y, v.y), Math.max(max.z, v.z));
}
boundingBox.setMinMax(min, max);
}
@Override
public BoundingVolume getModelBound() {
this.updateModelBound();
return boundingBox;
}
@Override
public BoundingVolume getWorldBound() {
this.updateModelBound();
Node parent = this.getParent();
if (parent != null) {
BoundingVolume bv = boundingBox.clone();
bv.setCenter(parent.getWorldTranslation());
return bv;
} else {
return boundingBox;
}
}
/**
* Triangulates the mesh.
*/
public void triangulate() {
Set<TriangulationWarning> warnings = new HashSet<>(TriangulationWarning.values().length - 1);
LOGGER.fine("Triangulating temporal mesh.");
for (Face face : faces) {
TriangulationWarning warning = face.triangulate();
if(warning != TriangulationWarning.NONE) {
warnings.add(warning);
}
}
if(warnings.size() > 0 && LOGGER.isLoggable(Level.WARNING)) {
StringBuilder sb = new StringBuilder(512);
sb.append("There were problems with triangulating the faces of a mesh: ").append(name);
for(TriangulationWarning w : warnings) {
sb.append("\n\t").append(w);
}
LOGGER.warning(sb.toString());
}
}
/**
* The method appends the given mesh to the current one. New faces and vertices and indexes are added.
* @param mesh
* the mesh to be appended
*/
public void append(TemporalMesh mesh) {
if (mesh != null) {
// we need to shift the indexes in faces, lines and points
int shift = vertices.size();
if (shift > 0) {
for (Face face : mesh.faces) {
face.getIndexes().shiftIndexes(shift, null);
face.setTemporalMesh(this);
}
for (Edge edge : mesh.edges) {
edge.shiftIndexes(shift, null);
}
for (Point point : mesh.points) {
point.shiftIndexes(shift, null);
}
}
faces.addAll(mesh.faces);
edges.addAll(mesh.edges);
points.addAll(mesh.points);
vertices.addAll(mesh.vertices);
normals.addAll(mesh.normals);
vertexGroups.addAll(mesh.vertexGroups);
verticesColors.addAll(mesh.verticesColors);
boneIndexes.putAll(mesh.boneIndexes);
this.rebuildIndexesMappings();
}
}
/**
* Sets the properties of the mesh.
* @param properties
* the properties of the mesh
*/
public void setProperties(Properties properties) {
this.properties = properties;
}
/**
* Sets the materials of the mesh.
* @param materials
* the materials of the mesh
*/
public void setMaterials(MaterialContext[] materials) {
this.materials = materials;
}
/**
* Adds bone index to the mesh.
* @param boneName
* the name of the bone
* @param boneIndex
* the index of the bone
*/
public void addBoneIndex(String boneName, Integer boneIndex) {
boneIndexes.put(boneName, boneIndex);
}
/**
* The modifier to be applied after the geometries are created.
* @param modifier
* the modifier to be applied
*/
public void applyAfterMeshCreate(Modifier modifier) {
postMeshCreationModifiers.add(modifier);
}
@Override
public int getVertexCount() {
return vertices.size();
}
/**
* Removes all vertices from the mesh.
*/
public void clear() {
vertices.clear();
normals.clear();
vertexGroups.clear();
verticesColors.clear();
faces.clear();
edges.clear();
points.clear();
indexToEdgeMapping.clear();
indexToFaceMapping.clear();
}
/**
* The mesh builds geometries from the mesh. The result is stored in the blender context
* under the mesh's OMA.
*/
public void toGeometries() {
LOGGER.log(Level.FINE, "Converting temporal mesh {0} to jme geometries.", name);
List<Geometry> result = new ArrayList<Geometry>();
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
Node parent = this.getParent();
parent.detachChild(this);
this.prepareFacesGeometry(result, meshHelper);
this.prepareLinesGeometry(result, meshHelper);
this.preparePointsGeometry(result, meshHelper);
blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result);
for (Geometry geometry : result) {
parent.attachChild(geometry);
}
for (Modifier modifier : postMeshCreationModifiers) {
modifier.postMeshCreationApply(parent, blenderContext);
}
}
/**
* The method creates geometries from faces.
* @param result
* the list where new geometries will be appended
* @param meshHelper
* the mesh helper
*/
protected void prepareFacesGeometry(List<Geometry> result, MeshHelper meshHelper) {
LOGGER.fine("Preparing faces geometries.");
this.triangulate();
Vector3f[] tempVerts = new Vector3f[3];
Vector3f[] tempNormals = new Vector3f[3];
byte[][] tempVertColors = new byte[3][];
List<Map<Float, Integer>> boneBuffers = new ArrayList<Map<Float, Integer>>(3);
LOGGER.log(Level.FINE, "Appending {0} faces to mesh buffers.", faces.size());
Map<Integer, MeshBuffers> faceMeshes = new HashMap<Integer, MeshBuffers>();
for (Face face : faces) {
MeshBuffers meshBuffers = faceMeshes.get(face.getMaterialNumber());
if (meshBuffers == null) {
meshBuffers = new MeshBuffers(face.getMaterialNumber());
faceMeshes.put(face.getMaterialNumber(), meshBuffers);
}
List<List<Integer>> triangulatedIndexes = face.getCurrentIndexes();
List<byte[]> vertexColors = face.getVertexColors();
for (List<Integer> indexes : triangulatedIndexes) {
assert indexes.size() == 3 : "The mesh has not been properly triangulated!";
Vector3f normal = null;
if(!face.isSmooth()) {
normal = FastMath.computeNormal(vertices.get(indexes.get(0)), vertices.get(indexes.get(1)), vertices.get(indexes.get(2)));
}
boneBuffers.clear();
for (int i = 0; i < 3; ++i) {
int vertIndex = indexes.get(i);
tempVerts[i] = vertices.get(vertIndex);
tempNormals[i] = normal != null ? normal : normals.get(vertIndex);
tempVertColors[i] = vertexColors != null ? vertexColors.get(face.getIndexes().indexOf(vertIndex)) : null;
if (boneIndexes.size() > 0 && vertexGroups.size() > 0) {
Map<Float, Integer> boneBuffersForVertex = new HashMap<Float, Integer>();
Map<String, Float> vertexGroupsForVertex = vertexGroups.get(vertIndex);
for (Entry<String, Integer> entry : boneIndexes.entrySet()) {
if (vertexGroupsForVertex.containsKey(entry.getKey())) {
float weight = vertexGroupsForVertex.get(entry.getKey());
if (weight > MINIMUM_BONE_WEIGHT) {
// only values of weight greater than MINIMUM_BONE_WEIGHT are used
// if all non zero weights were used, and they were samm enough, problems with normalisation would occur
// because adding a very small value to 1.0 will give 1.0
// so in order to avoid such errors, which can cause severe animation artifacts we need to use some minimum weight value
boneBuffersForVertex.put(weight, entry.getValue());
}
}
}
if (boneBuffersForVertex.size() == 0) {// attach the vertex to zero-indexed bone so that it does not collapse to (0, 0, 0)
boneBuffersForVertex.put(1.0f, 0);
}
boneBuffers.add(boneBuffersForVertex);
}
}
Map<String, List<Vector2f>> uvs = meshHelper.selectUVSubset(face, indexes.toArray(new Integer[indexes.size()]));
meshBuffers.append(face.isSmooth(), tempVerts, tempNormals, uvs, tempVertColors, boneBuffers);
}
}
LOGGER.fine("Converting mesh buffers to geometries.");
Map<Geometry, MeshBuffers> geometryToBuffersMap = new HashMap<Geometry, MeshBuffers>();
for (Entry<Integer, MeshBuffers> entry : faceMeshes.entrySet()) {
MeshBuffers meshBuffers = entry.getValue();
Mesh mesh = new Mesh();
if (meshBuffers.isShortIndexBuffer()) {
mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer());
} else {
mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer());
}
mesh.setBuffer(meshBuffers.getPositionsBuffer());
mesh.setBuffer(meshBuffers.getNormalsBuffer());
if (meshBuffers.areVertexColorsUsed()) {
mesh.setBuffer(Type.Color, 4, meshBuffers.getVertexColorsBuffer());
mesh.getBuffer(Type.Color).setNormalized(true);
}
BoneBuffersData boneBuffersData = meshBuffers.getBoneBuffers();
if (boneBuffersData != null) {
mesh.setMaxNumWeights(boneBuffersData.maximumWeightsPerVertex);
mesh.setBuffer(boneBuffersData.verticesWeights);
mesh.setBuffer(boneBuffersData.verticesWeightsIndices);
LOGGER.fine("Generating bind pose and normal buffers.");
mesh.generateBindPose(true);
// change the usage type of vertex and normal buffers from Static to Stream
mesh.getBuffer(Type.Position).setUsage(Usage.Stream);
mesh.getBuffer(Type.Normal).setUsage(Usage.Stream);
// creating empty buffers for HW skinning; the buffers will be setup if ever used
VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight);
VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex);
mesh.setBuffer(verticesWeightsHW);
mesh.setBuffer(verticesWeightsIndicesHW);
}
Geometry geometry = new Geometry(name + (result.size() + 1), mesh);
if (properties != null && properties.getValue() != null) {
meshHelper.applyProperties(geometry, properties);
}
result.add(geometry);
geometryToBuffersMap.put(geometry, meshBuffers);
}
LOGGER.fine("Applying materials to geometries.");
for (Entry<Geometry, MeshBuffers> entry : geometryToBuffersMap.entrySet()) {
int materialIndex = entry.getValue().getMaterialIndex();
Geometry geometry = entry.getKey();
if (materialIndex >= 0 && materials != null && materials.length > materialIndex && materials[materialIndex] != null) {
materials[materialIndex].applyMaterial(geometry, meshStructure.getOldMemoryAddress(), entry.getValue().getUvCoords(), blenderContext);
} else {
Material defaultMaterial = blenderContext.getDefaultMaterial().clone();
defaultMaterial.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
geometry.setMaterial(defaultMaterial);
}
}
}
/**
* The method creates geometries from lines.
* @param result
* the list where new geometries will be appended
* @param meshHelper
* the mesh helper
*/
protected void prepareLinesGeometry(List<Geometry> result, MeshHelper meshHelper) {
if (edges.size() > 0) {
LOGGER.fine("Preparing lines geometries.");
List<List<Integer>> separateEdges = new ArrayList<List<Integer>>();
List<Edge> edges = new ArrayList<Edge>(this.edges.size());
for (Edge edge : this.edges) {
if (!edge.isInFace()) {
edges.add(edge);
}
}
while (edges.size() > 0) {
boolean edgeAppended = false;
int edgeIndex = 0;
for (List<Integer> list : separateEdges) {
for (edgeIndex = 0; edgeIndex < edges.size() && !edgeAppended; ++edgeIndex) {
Edge edge = edges.get(edgeIndex);
if (list.get(0).equals(edge.getFirstIndex())) {
list.add(0, edge.getSecondIndex());
--edgeIndex;
edgeAppended = true;
} else if (list.get(0).equals(edge.getSecondIndex())) {
list.add(0, edge.getFirstIndex());
--edgeIndex;
edgeAppended = true;
} else if (list.get(list.size() - 1).equals(edge.getFirstIndex())) {
list.add(edge.getSecondIndex());
--edgeIndex;
edgeAppended = true;
} else if (list.get(list.size() - 1).equals(edge.getSecondIndex())) {
list.add(edge.getFirstIndex());
--edgeIndex;
edgeAppended = true;
}
}
if (edgeAppended) {
break;
}
}
Edge edge = edges.remove(edgeAppended ? edgeIndex : 0);
if (!edgeAppended) {
separateEdges.add(new ArrayList<Integer>(Arrays.asList(edge.getFirstIndex(), edge.getSecondIndex())));
}
}
for (List<Integer> list : separateEdges) {
MeshBuffers meshBuffers = new MeshBuffers(0);
for (int index : list) {
meshBuffers.append(vertices.get(index), normals.get(index));
}
Mesh mesh = new Mesh();
mesh.setLineWidth(blenderContext.getBlenderKey().getLinesWidth());
mesh.setMode(Mode.LineStrip);
if (meshBuffers.isShortIndexBuffer()) {
mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer());
} else {
mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer());
}
mesh.setBuffer(meshBuffers.getPositionsBuffer());
mesh.setBuffer(meshBuffers.getNormalsBuffer());
Geometry geometry = new Geometry(meshStructure.getName() + (result.size() + 1), mesh);
geometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext));
if (properties != null && properties.getValue() != null) {
meshHelper.applyProperties(geometry, properties);
}
result.add(geometry);
}
}
}
/**
* The method creates geometries from points.
* @param result
* the list where new geometries will be appended
* @param meshHelper
* the mesh helper
*/
protected void preparePointsGeometry(List<Geometry> result, MeshHelper meshHelper) {
if (points.size() > 0) {
LOGGER.fine("Preparing point geometries.");
MeshBuffers pointBuffers = new MeshBuffers(0);
for (Point point : points) {
pointBuffers.append(vertices.get(point.getIndex()), normals.get(point.getIndex()));
}
Mesh pointsMesh = new Mesh();
pointsMesh.setMode(Mode.Points);
pointsMesh.setPointSize(blenderContext.getBlenderKey().getPointsSize());
if (pointBuffers.isShortIndexBuffer()) {
pointsMesh.setBuffer(Type.Index, 1, (ShortBuffer) pointBuffers.getIndexBuffer());
} else {
pointsMesh.setBuffer(Type.Index, 1, (IntBuffer) pointBuffers.getIndexBuffer());
}
pointsMesh.setBuffer(pointBuffers.getPositionsBuffer());
pointsMesh.setBuffer(pointBuffers.getNormalsBuffer());
Geometry pointsGeometry = new Geometry(meshStructure.getName() + (result.size() + 1), pointsMesh);
pointsGeometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext));
if (properties != null && properties.getValue() != null) {
meshHelper.applyProperties(pointsGeometry, properties);
}
result.add(pointsGeometry);
}
}
@Override
public String toString() {
return "TemporalMesh [name=" + name + ", vertices.size()=" + vertices.size() + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (meshStructure == null ? 0 : meshStructure.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof TemporalMesh)) {
return false;
}
TemporalMesh other = (TemporalMesh) obj;
return meshStructure.getOldMemoryAddress().equals(other.meshStructure.getOldMemoryAddress());
}
}

@ -0,0 +1,113 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.AnimationHelper;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
/**
* This modifier allows to add bone animation to the object.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ArmatureModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(ArmatureModifier.class.getName());
private static final int FLAG_VERTEX_GROUPS = 0x01;
private static final int FLAG_BONE_ENVELOPES = 0x02;
private Skeleton skeleton;
/**
* This constructor reads animation data from the object structore. The
* stored data is the AnimData and additional data is armature's OMA.
*
* @param objectStructure
* the structure of the object
* @param modifierStructure
* the structure of the modifier
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public ArmatureModifier(Structure objectStructure, Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
if (this.validate(modifierStructure, blenderContext)) {
Pointer pArmatureObject = (Pointer) modifierStructure.getFieldValue("object");
if (pArmatureObject.isNotNull()) {
int deformflag = ((Number) modifierStructure.getFieldValue("deformflag")).intValue();
boolean useVertexGroups = (deformflag & FLAG_VERTEX_GROUPS) != 0;
boolean useBoneEnvelopes = (deformflag & FLAG_BONE_ENVELOPES) != 0;
modifying = useBoneEnvelopes || useVertexGroups;
if (modifying) {// if neither option is used the modifier will not modify anything anyway
Structure armatureObject = pArmatureObject.fetchData().get(0);
if(blenderContext.getSkeleton(armatureObject.getOldMemoryAddress()) == null) {
LOGGER.fine("Creating new skeleton for armature modifier.");
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData().get(0);
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase();
List<Bone> bonesList = new ArrayList<Bone>();
for (int i = 0; i < bonebase.size(); ++i) {
this.buildBones(armatureObject.getOldMemoryAddress(), bonebase.get(i), null, bonesList, objectStructure.getOldMemoryAddress(), blenderContext);
}
bonesList.add(0, new Bone(""));
Bone[] bones = bonesList.toArray(new Bone[bonesList.size()]);
skeleton = new Skeleton(bones);
blenderContext.setSkeleton(armatureObject.getOldMemoryAddress(), skeleton);
} else {
skeleton = blenderContext.getSkeleton(armatureObject.getOldMemoryAddress());
}
}
} else {
modifying = false;
}
}
}
private void buildBones(Long armatureObjectOMA, Structure boneStructure, Bone parent, List<Bone> result, Long spatialOMA, BlenderContext blenderContext) throws BlenderFileException {
BoneContext bc = new BoneContext(armatureObjectOMA, boneStructure, blenderContext);
bc.buildBone(result, spatialOMA, blenderContext);
}
@Override
public void postMeshCreationApply(Node node, BlenderContext blenderContext) {
LOGGER.fine("Applying armature modifier after mesh has been created.");
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
animationHelper.applyAnimations(node, skeleton, blenderContext.getBlenderKey().getAnimationMatchMethod());
node.updateModelBound();
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Armature modifier is invalid! Cannot be applied to: {0}", node.getName());
}
if (modifying) {
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if (temporalMesh != null) {
LOGGER.log(Level.FINE, "Applying armature modifier to: {0}", temporalMesh);
LOGGER.fine("Creating map between bone name and its index.");
for (int i = 0; i < skeleton.getBoneCount(); ++i) {
Bone bone = skeleton.getBone(i);
temporalMesh.addBoneIndex(bone.getName(), i);
}
temporalMesh.applyAfterMeshCreate(this);
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}
}

@ -0,0 +1,241 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.MeshHelper;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.scene.shape.Curve;
/**
* This modifier allows to array modifier to the object.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ArrayModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(ArrayModifier.class.getName());
private int fittype;
private int count;
private float length;
private float[] offset;
private float[] scale;
private Pointer pOffsetObject;
private Pointer pStartCap;
private Pointer pEndCap;
/**
* This constructor reads array data from the modifier structure. The
* stored data is a map of parameters for array modifier. No additional data
* is loaded.
*
* @param objectStructure
* the structure of the object
* @param modifierStructure
* the structure of the modifier
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
@SuppressWarnings("unchecked")
public ArrayModifier(Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
if (this.validate(modifierStructure, blenderContext)) {
fittype = ((Number) modifierStructure.getFieldValue("fit_type")).intValue();
switch (fittype) {
case 0:// FIXED COUNT
count = ((Number) modifierStructure.getFieldValue("count")).intValue();
break;
case 1:// FIXED LENGTH
length = ((Number) modifierStructure.getFieldValue("length")).floatValue();
break;
case 2:// FITCURVE
Pointer pCurveOb = (Pointer) modifierStructure.getFieldValue("curve_ob");
if (pCurveOb.isNotNull()) {
Structure curveStructure = pCurveOb.fetchData().get(0);
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Node curveObject = (Node) objectHelper.toObject(curveStructure, blenderContext);
Set<Number> referencesToCurveLengths = new HashSet<Number>(curveObject.getChildren().size());
for (Spatial spatial : curveObject.getChildren()) {
if (spatial instanceof Geometry) {
Mesh mesh = ((Geometry) spatial).getMesh();
if (mesh instanceof Curve) {
length += ((Curve) mesh).getLength();
} else {
// if bevel object has several parts then each mesh will have the same reference
// to length value (and we should use only one)
Number curveLength = spatial.getUserData("curveLength");
if (curveLength != null && !referencesToCurveLengths.contains(curveLength)) {
length += curveLength.floatValue();
referencesToCurveLengths.add(curveLength);
}
}
}
}
}
fittype = 1;// treat it like FIXED LENGTH
break;
default:
assert false : "Unknown array modifier fit type: " + fittype;
}
// offset parameters
int offsettype = ((Number) modifierStructure.getFieldValue("offset_type")).intValue();
if ((offsettype & 0x01) != 0) {// Constant offset
DynamicArray<Number> offsetArray = (DynamicArray<Number>) modifierStructure.getFieldValue("offset");
offset = new float[] { offsetArray.get(0).floatValue(), offsetArray.get(1).floatValue(), offsetArray.get(2).floatValue() };
}
if ((offsettype & 0x02) != 0) {// Relative offset
DynamicArray<Number> scaleArray = (DynamicArray<Number>) modifierStructure.getFieldValue("scale");
scale = new float[] { scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue() };
}
if ((offsettype & 0x04) != 0) {// Object offset
pOffsetObject = (Pointer) modifierStructure.getFieldValue("offset_ob");
}
// start cap and end cap
pStartCap = (Pointer) modifierStructure.getFieldValue("start_cap");
pEndCap = (Pointer) modifierStructure.getFieldValue("end_cap");
}
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Array modifier is invalid! Cannot be applied to: {0}", node.getName());
} else {
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if (temporalMesh != null) {
LOGGER.log(Level.FINE, "Applying array modifier to: {0}", temporalMesh);
if (offset == null) {// the node will be repeated several times in the same place
offset = new float[] { 0.0f, 0.0f, 0.0f };
}
if (scale == null) {// the node will be repeated several times in the same place
scale = new float[] { 0.0f, 0.0f, 0.0f };
} else {
// getting bounding box
temporalMesh.updateModelBound();
BoundingVolume boundingVolume = temporalMesh.getWorldBound();
if (boundingVolume instanceof BoundingBox) {
scale[0] *= ((BoundingBox) boundingVolume).getXExtent() * 2.0f;
scale[1] *= ((BoundingBox) boundingVolume).getYExtent() * 2.0f;
scale[2] *= ((BoundingBox) boundingVolume).getZExtent() * 2.0f;
} else if (boundingVolume instanceof BoundingSphere) {
float radius = ((BoundingSphere) boundingVolume).getRadius();
scale[0] *= radius * 2.0f;
scale[1] *= radius * 2.0f;
scale[2] *= radius * 2.0f;
} else {
throw new IllegalStateException("Unknown bounding volume type: " + boundingVolume.getClass().getName());
}
}
// adding object's offset
float[] objectOffset = new float[] { 0.0f, 0.0f, 0.0f };
if (pOffsetObject != null && pOffsetObject.isNotNull()) {
FileBlockHeader offsetObjectBlock = blenderContext.getFileBlock(pOffsetObject.getOldMemoryAddress());
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
try {// we take the structure in case the object was not yet loaded
Structure offsetStructure = offsetObjectBlock.getStructure(blenderContext);
Vector3f translation = objectHelper.getTransformation(offsetStructure, blenderContext).getTranslation();
objectOffset[0] = translation.x;
objectOffset[1] = translation.y;
objectOffset[2] = translation.z;
} catch (BlenderFileException e) {
LOGGER.log(Level.WARNING, "Problems in blender file structure! Object offset cannot be applied! The problem: {0}", e.getMessage());
}
}
// getting start and end caps
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
TemporalMesh[] caps = new TemporalMesh[] { null, null };
Pointer[] pCaps = new Pointer[] { pStartCap, pEndCap };
for (int i = 0; i < pCaps.length; ++i) {
if (pCaps[i].isNotNull()) {
FileBlockHeader capBlock = blenderContext.getFileBlock(pCaps[i].getOldMemoryAddress());
try {// we take the structure in case the object was not yet loaded
Structure capStructure = capBlock.getStructure(blenderContext);
Pointer pMesh = (Pointer) capStructure.getFieldValue("data");
List<Structure> meshesArray = pMesh.fetchData();
caps[i] = meshHelper.toTemporalMesh(meshesArray.get(0), blenderContext);
} catch (BlenderFileException e) {
LOGGER.log(Level.WARNING, "Problems in blender file structure! Cap object cannot be applied! The problem: {0}", e.getMessage());
}
}
}
Vector3f translationVector = new Vector3f(offset[0] + scale[0] + objectOffset[0], offset[1] + scale[1] + objectOffset[1], offset[2] + scale[2] + objectOffset[2]);
if (blenderContext.getBlenderKey().isFixUpAxis()) {
float y = translationVector.y;
translationVector.y = translationVector.z;
translationVector.z = y == 0 ? 0 : -y;
}
// getting/calculating repeats amount
int count = 0;
if (fittype == 0) {// Fixed count
count = this.count - 1;
} else if (fittype == 1) {// Fixed length
float length = this.length;
if (translationVector.length() > 0.0f) {
count = (int) (length / translationVector.length()) - 1;
}
} else if (fittype == 2) {// Fit curve
throw new IllegalStateException("Fit curve should be transformed to Fixed Length array type!");
} else {
throw new IllegalStateException("Unknown fit type: " + fittype);
}
// adding translated nodes and caps
Vector3f totalTranslation = new Vector3f(translationVector);
if (count > 0) {
TemporalMesh originalMesh = temporalMesh.clone();
for (int i = 0; i < count; ++i) {
TemporalMesh clone = originalMesh.clone();
for (Vector3f v : clone.getVertices()) {
v.addLocal(totalTranslation);
}
temporalMesh.append(clone);
totalTranslation.addLocal(translationVector);
}
}
if (caps[0] != null) {
translationVector.multLocal(-1);
TemporalMesh capsClone = caps[0].clone();
for (Vector3f v : capsClone.getVertices()) {
v.addLocal(translationVector);
}
temporalMesh.append(capsClone);
}
if (caps[1] != null) {
TemporalMesh capsClone = caps[1].clone();
for (Vector3f v : capsClone.getVertices()) {
v.addLocal(totalTranslation);
}
temporalMesh.append(capsClone);
}
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}
}

@ -0,0 +1,200 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.Edge;
import com.jme3.scene.plugins.blender.meshes.Face;
import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate;
import com.jme3.scene.plugins.blender.meshes.Point;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
/**
* This modifier allows to use mask modifier on the object.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class MaskModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(MaskModifier.class.getName());
private static final int FLAG_INVERT_MASK = 0x01;
private static final int MODE_VERTEX_GROUP = 0;
private static final int MODE_ARMATURE = 1;
private Pointer pArmatureObject;
private String vertexGroupName;
private boolean invertMask;
public MaskModifier(Structure modifierStructure, BlenderContext blenderContext) {
if (this.validate(modifierStructure, blenderContext)) {
int flag = ((Number) modifierStructure.getFieldValue("flag")).intValue();
invertMask = (flag & FLAG_INVERT_MASK) != 0;
int mode = ((Number) modifierStructure.getFieldValue("mode")).intValue();
if (mode == MODE_VERTEX_GROUP) {
vertexGroupName = modifierStructure.getFieldValue("vgroup").toString();
if (vertexGroupName != null && vertexGroupName.length() == 0) {
vertexGroupName = null;
}
} else if (mode == MODE_ARMATURE) {
pArmatureObject = (Pointer) modifierStructure.getFieldValue("ob_arm");
} else {
LOGGER.log(Level.SEVERE, "Unknown mode type: {0}. Cannot apply modifier: {1}.", new Object[] { mode, modifierStructure.getName() });
invalid = true;
}
}
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Mirror modifier is invalid! Cannot be applied to: {0}", node.getName());
} else {
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if (temporalMesh != null) {
List<String> vertexGroupsToRemove = new ArrayList<String>();
if (vertexGroupName != null) {
vertexGroupsToRemove.add(vertexGroupName);
} else if (pArmatureObject != null && pArmatureObject.isNotNull()) {
try {
Structure armatureObject = pArmatureObject.fetchData().get(0);
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData().get(0);
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase();
vertexGroupsToRemove.addAll(this.readBoneNames(bonebase));
} catch (BlenderFileException e) {
LOGGER.log(Level.SEVERE, "Cannot load armature object for the mask modifier. Cause: {0}", e.getLocalizedMessage());
LOGGER.log(Level.SEVERE, "Mask modifier will NOT be applied to node named: {0}", node.getName());
}
} else {
// if the mesh has no vertex groups then remove all verts
// if the mesh has at least one vertex group - then do nothing
// I have no idea why we should do that, but blender works this way
Set<String> vertexGroupNames = new HashSet<String>();
for (Map<String, Float> groups : temporalMesh.getVertexGroups()) {
vertexGroupNames.addAll(groups.keySet());
}
if (vertexGroupNames.size() == 0 && !invertMask || vertexGroupNames.size() > 0 && invertMask) {
temporalMesh.clear();
}
}
if (vertexGroupsToRemove.size() > 0) {
List<Integer> vertsToBeRemoved = new ArrayList<Integer>();
for (int i = 0; i < temporalMesh.getVertexCount(); ++i) {
Map<String, Float> vertexGroups = temporalMesh.getVertexGroups().get(i);
boolean hasVertexGroup = false;
if (vertexGroups != null) {
for (String groupName : vertexGroupsToRemove) {
Float weight = vertexGroups.get(groupName);
if (weight != null && weight > 0) {
hasVertexGroup = true;
break;
}
}
}
if (!hasVertexGroup && !invertMask || hasVertexGroup && invertMask) {
vertsToBeRemoved.add(i);
}
}
Collections.reverse(vertsToBeRemoved);
for (Integer vertexIndex : vertsToBeRemoved) {
this.removeVertexAt(vertexIndex, temporalMesh);
}
}
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}
/**
* Every face, edge and point that contains
* the vertex will be removed.
* @param index
* the index of a vertex to be removed
* @throws IndexOutOfBoundsException
* thrown when given index is negative or beyond the count of vertices
*/
private void removeVertexAt(final int index, TemporalMesh temporalMesh) {
if (index < 0 || index >= temporalMesh.getVertexCount()) {
throw new IndexOutOfBoundsException("The given index is out of bounds: " + index);
}
temporalMesh.getVertices().remove(index);
temporalMesh.getNormals().remove(index);
if (temporalMesh.getVertexGroups().size() > 0) {
temporalMesh.getVertexGroups().remove(index);
}
if (temporalMesh.getVerticesColors().size() > 0) {
temporalMesh.getVerticesColors().remove(index);
}
IndexPredicate shiftPredicate = new IndexPredicate() {
@Override
public boolean execute(Integer i) {
return i > index;
}
};
for (int i = temporalMesh.getFaces().size() - 1; i >= 0; --i) {
Face face = temporalMesh.getFaces().get(i);
if (face.getIndexes().indexOf(index) >= 0) {
temporalMesh.getFaces().remove(i);
} else {
face.getIndexes().shiftIndexes(-1, shiftPredicate);
}
}
for (int i = temporalMesh.getEdges().size() - 1; i >= 0; --i) {
Edge edge = temporalMesh.getEdges().get(i);
if (edge.getFirstIndex() == index || edge.getSecondIndex() == index) {
temporalMesh.getEdges().remove(i);
} else {
edge.shiftIndexes(-1, shiftPredicate);
}
}
for (int i = temporalMesh.getPoints().size() - 1; i >= 0; --i) {
Point point = temporalMesh.getPoints().get(i);
if (point.getIndex() == index) {
temporalMesh.getPoints().remove(i);
} else {
point.shiftIndexes(-1, shiftPredicate);
}
}
}
/**
* Reads the names of the bones from the given bone base.
* @param boneBase
* the list of bone base structures
* @return a list of bones' names
* @throws BlenderFileException
* is thrown if problems with reading the child bones' bases occur
*/
private List<String> readBoneNames(List<Structure> boneBase) throws BlenderFileException {
List<String> result = new ArrayList<String>();
for (Structure boneStructure : boneBase) {
int flag = ((Number) boneStructure.getFieldValue("flag")).intValue();
if ((flag & BoneContext.SELECTED) != 0) {
result.add(boneStructure.getFieldValue("name").toString());
}
List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase();
result.addAll(this.readBoneNames(childbase));
}
return result;
}
}

@ -0,0 +1,196 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.Matrix4f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.Edge;
import com.jme3.scene.plugins.blender.meshes.Face;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
/**
* This modifier allows to array modifier to the object.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class MirrorModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(MirrorModifier.class.getName());
private static final int FLAG_MIRROR_X = 0x08;
private static final int FLAG_MIRROR_Y = 0x10;
private static final int FLAG_MIRROR_Z = 0x20;
private static final int FLAG_MIRROR_U = 0x02;
private static final int FLAG_MIRROR_V = 0x04;
private static final int FLAG_MIRROR_VERTEX_GROUP = 0x40;
private static final int FLAG_MIRROR_MERGE = 0x80;
private boolean[] isMirrored;
private boolean mirrorU, mirrorV;
private boolean merge;
private float tolerance;
private Pointer pMirrorObject;
private boolean mirrorVGroup;
/**
* This constructor reads mirror data from the modifier structure. The
* stored data is a map of parameters for mirror modifier. No additional data
* is loaded.
* When the modifier is applied it is necessary to get the newly created node.
*
* @param objectStructure
* the structure of the object
* @param modifierStructure
* the structure of the modifier
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public MirrorModifier(Structure modifierStructure, BlenderContext blenderContext) {
if (this.validate(modifierStructure, blenderContext)) {
int flag = ((Number) modifierStructure.getFieldValue("flag")).intValue();
isMirrored = new boolean[] { (flag & FLAG_MIRROR_X) != 0, (flag & FLAG_MIRROR_Y) != 0, (flag & FLAG_MIRROR_Z) != 0 };
if (blenderContext.getBlenderKey().isFixUpAxis()) {
boolean temp = isMirrored[1];
isMirrored[1] = isMirrored[2];
isMirrored[2] = temp;
}
mirrorU = (flag & FLAG_MIRROR_U) != 0;
mirrorV = (flag & FLAG_MIRROR_V) != 0;
mirrorVGroup = (flag & FLAG_MIRROR_VERTEX_GROUP) != 0;
merge = (flag & FLAG_MIRROR_MERGE) == 0;// in this case we use == instead of != (this is not a mistake)
tolerance = ((Number) modifierStructure.getFieldValue("tolerance")).floatValue();
pMirrorObject = (Pointer) modifierStructure.getFieldValue("mirror_ob");
if (mirrorVGroup) {
LOGGER.warning("Mirroring vertex groups is currently not supported.");
}
}
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Mirror modifier is invalid! Cannot be applied to: {0}", node.getName());
} else {
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if (temporalMesh != null) {
LOGGER.log(Level.FINE, "Applying mirror modifier to: {0}", temporalMesh);
Vector3f mirrorPlaneCenter = new Vector3f();
if (pMirrorObject.isNotNull()) {
Structure objectStructure;
try {
objectStructure = pMirrorObject.fetchData().get(0);
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Node object = (Node) objectHelper.toObject(objectStructure, blenderContext);
if (object != null) {
// compute the mirror object coordinates in node's local space
mirrorPlaneCenter = this.getWorldMatrix(node).invertLocal().mult(object.getWorldTranslation());
}
} catch (BlenderFileException e) {
LOGGER.log(Level.SEVERE, "Cannot load mirror''s reference object. Cause: {0}", e.getLocalizedMessage());
LOGGER.log(Level.SEVERE, "Mirror modifier will not be applied to node named: {0}", node.getName());
return;
}
}
LOGGER.finest("Allocating temporal variables.");
float d;
Vector3f mirrorPlaneNormal = new Vector3f();
Vector3f shiftVector = new Vector3f();
LOGGER.fine("Mirroring mesh.");
for (int mirrorIndex = 0; mirrorIndex < 3; ++mirrorIndex) {
if (isMirrored[mirrorIndex]) {
boolean mirrorAtPoint0 = mirrorPlaneCenter.get(mirrorIndex) == 0;
if (!mirrorAtPoint0) {// compute mirror's plane normal vector in node's space
mirrorPlaneNormal.set(0, 0, 0).set(mirrorIndex, Math.signum(mirrorPlaneCenter.get(mirrorIndex)));
}
TemporalMesh mirror = temporalMesh.clone();
for (int i = 0; i < mirror.getVertexCount(); ++i) {
Vector3f vertex = mirror.getVertices().get(i);
Vector3f normal = mirror.getNormals().get(i);
if (mirrorAtPoint0) {
d = Math.abs(vertex.get(mirrorIndex));
shiftVector.set(0, 0, 0).set(mirrorIndex, -vertex.get(mirrorIndex));
} else {
d = this.computeDistanceFromPlane(vertex, mirrorPlaneCenter, mirrorPlaneNormal);
mirrorPlaneNormal.mult(d, shiftVector);
}
if (merge && d <= tolerance) {
vertex.addLocal(shiftVector);
normal.set(mirrorIndex, 0);
temporalMesh.getVertices().get(i).addLocal(shiftVector);
temporalMesh.getNormals().get(i).set(mirrorIndex, 0);
} else {
vertex.addLocal(shiftVector.multLocal(2));
normal.set(mirrorIndex, -normal.get(mirrorIndex));
}
}
// flipping the indexes
for (Face face : mirror.getFaces()) {
face.flipIndexes();
}
for (Edge edge : mirror.getEdges()) {
edge.flipIndexes();
}
Collections.reverse(mirror.getPoints());
if (mirrorU || mirrorV) {
for (Face face : mirror.getFaces()) {
face.flipUV(mirrorU, mirrorV);
}
}
temporalMesh.append(mirror);
}
}
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}
/**
* Fetches the world matrix transformation of the given node.
* @param node
* the node
* @return the node's world transformation matrix
*/
private Matrix4f getWorldMatrix(Node node) {
Matrix4f result = new Matrix4f();
result.setTranslation(node.getWorldTranslation());
result.setRotationQuaternion(node.getWorldRotation());
result.setScale(node.getWorldScale());
return result;
}
/**
* The method computes the distance between a point and a plane (described by point in space and normal vector).
* @param p
* the point in the space
* @param c
* mirror's plane center
* @param n
* mirror's plane normal (should be normalized)
* @return the minimum distance from point to plane
*/
private float computeDistanceFromPlane(Vector3f p, Vector3f c, Vector3f n) {
return Math.abs(n.dot(p) - c.dot(n));
}
}

@ -0,0 +1,92 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.List;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
/**
* This class represents an object's modifier. The modifier object can be varied
* and the user needs to know what is the type of it for the specified type
* name. For example "ArmatureModifierData" type specified in blender is
* represented by AnimData object from jMonkeyEngine.
*
* @author Marcin Roguski (Kaelthas)
*/
public abstract class Modifier {
public static final String ARRAY_MODIFIER_DATA = "ArrayModifierData";
public static final String ARMATURE_MODIFIER_DATA = "ArmatureModifierData";
public static final String PARTICLE_MODIFIER_DATA = "ParticleSystemModifierData";
public static final String MIRROR_MODIFIER_DATA = "MirrorModifierData";
public static final String SUBSURF_MODIFIER_DATA = "SubsurfModifierData";
public static final String OBJECT_ANIMATION_MODIFIER_DATA = "ObjectAnimationModifierData";
/** This variable indicates if the modifier is invalid (<b>true</b>) or not (<b>false</b>). */
protected boolean invalid;
/**
* A variable that tells if the modifier causes modification. Some modifiers like ArmatureModifier might have no
* Armature object attached and thus not really modifying the feature. In such cases it is good to know if it is
* sense to add the modifier to the list of object's modifiers.
*/
protected boolean modifying = true;
/**
* This method applies the modifier to the given node.
*
* @param node
* the node that will have modifier applied
* @param blenderContext
* the blender context
*/
public abstract void apply(Node node, BlenderContext blenderContext);
/**
* The method that is called when geometries are already created.
* @param node
* the node that will have the modifier applied
* @param blenderContext
* the blender context
*/
public void postMeshCreationApply(Node node, BlenderContext blenderContext) {
}
/**
* Determines if the modifier can be applied multiple times over one mesh.
* At this moment only armature and object animation modifiers cannot be
* applied multiple times.
*
* @param modifierType
* the type name of the modifier
* @return <b>true</b> if the modifier can be applied many times and
* <b>false</b> otherwise
*/
public static boolean canBeAppliedMultipleTimes(String modifierType) {
return !(ARMATURE_MODIFIER_DATA.equals(modifierType) || OBJECT_ANIMATION_MODIFIER_DATA.equals(modifierType));
}
protected boolean validate(Structure modifierStructure, BlenderContext blenderContext) {
Structure modifierData = (Structure) modifierStructure.getFieldValue("modifier");
Pointer pError = (Pointer) modifierData.getFieldValue("error");
invalid = pError.isNotNull();
return !invalid;
}
/**
* @return <b>true</b> if the modifier causes feature's modification or <b>false</b> if not
*/
public boolean isModifying() {
return modifying;
}
protected TemporalMesh getTemporalMesh(Node node) {
List<Spatial> children = node.getChildren();
if (children != null && children.size() == 1 && children.get(0) instanceof TemporalMesh) {
return (TemporalMesh) children.get(0);
}
return null;
}
}

@ -0,0 +1,117 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.modifiers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
/**
* A class that is used in modifiers calculations.
*
* @author Marcin Roguski
*/
public class ModifierHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(ModifierHelper.class.getName());
/**
* This constructor parses the given blender version and stores the result.
* Some functionalities may differ in different blender versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public ModifierHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method reads the given object's modifiers.
*
* @param objectStructure
* the object structure
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public Collection<Modifier> readModifiers(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException {
Set<String> alreadyReadModifiers = new HashSet<String>();
Collection<Modifier> result = new ArrayList<Modifier>();
Structure modifiersListBase = (Structure) objectStructure.getFieldValue("modifiers");
List<Structure> modifiers = modifiersListBase.evaluateListBase();
for (Structure modifierStructure : modifiers) {
String modifierType = modifierStructure.getType();
if (!Modifier.canBeAppliedMultipleTimes(modifierType) && alreadyReadModifiers.contains(modifierType)) {
LOGGER.log(Level.WARNING, "Modifier {0} can only be applied once to object: {1}", new Object[] { modifierType, objectStructure.getName() });
} else {
Modifier modifier = null;
if (Modifier.ARRAY_MODIFIER_DATA.equals(modifierStructure.getType())) {
modifier = new ArrayModifier(modifierStructure, blenderContext);
} else if (Modifier.MIRROR_MODIFIER_DATA.equals(modifierStructure.getType())) {
modifier = new MirrorModifier(modifierStructure, blenderContext);
} else if (Modifier.ARMATURE_MODIFIER_DATA.equals(modifierStructure.getType())) {
modifier = new ArmatureModifier(objectStructure, modifierStructure, blenderContext);
} else if (Modifier.PARTICLE_MODIFIER_DATA.equals(modifierStructure.getType())) {
modifier = new ParticlesModifier(modifierStructure, blenderContext);
} else if(Modifier.SUBSURF_MODIFIER_DATA.equals(modifierStructure.getType())) {
modifier = new SubdivisionSurfaceModifier(modifierStructure, blenderContext);
}
if (modifier != null) {
if (modifier.isModifying()) {
result.add(modifier);
alreadyReadModifiers.add(modifierType);
} else {
LOGGER.log(Level.WARNING, "The modifier {0} will cause no changes in the model. It will be ignored!", modifierStructure.getName());
}
} else {
LOGGER.log(Level.WARNING, "Unsupported modifier type: {0}", modifierStructure.getType());
}
}
}
return result;
}
}

@ -0,0 +1,107 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.shapes.EmitterMeshVertexShape;
import com.jme3.effect.shapes.EmitterShape;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.particles.ParticlesHelper;
/**
* This modifier allows to add particles to the object.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ParticlesModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(MirrorModifier.class.getName());
/** Loaded particles emitter. */
private ParticleEmitter particleEmitter;
/**
* This constructor reads the particles system structure and stores it in
* order to apply it later to the node.
*
* @param modifierStructure
* the structure of the modifier
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is throw wneh there are problems with the
* blender file
*/
public ParticlesModifier(Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
if (this.validate(modifierStructure, blenderContext)) {
Pointer pParticleSystem = (Pointer) modifierStructure.getFieldValue("psys");
if (pParticleSystem.isNotNull()) {
ParticlesHelper particlesHelper = blenderContext.getHelper(ParticlesHelper.class);
Structure particleSystem = pParticleSystem.fetchData().get(0);
particleEmitter = particlesHelper.toParticleEmitter(particleSystem);
}
}
}
@Override
public void postMeshCreationApply(Node node, BlenderContext blenderContext) {
LOGGER.log(Level.FINE, "Applying particles modifier to: {0}", node);
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
ParticleEmitter emitter = particleEmitter.clone();
// veryfying the alpha function for particles' texture
Integer alphaFunction = MaterialHelper.ALPHA_MASK_HYPERBOLE;
char nameSuffix = emitter.getName().charAt(emitter.getName().length() - 1);
if (nameSuffix == 'B' || nameSuffix == 'N') {
alphaFunction = MaterialHelper.ALPHA_MASK_NONE;
}
// removing the type suffix from the name
emitter.setName(emitter.getName().substring(0, emitter.getName().length() - 1));
// applying emitter shape
EmitterShape emitterShape = emitter.getShape();
List<Mesh> meshes = new ArrayList<Mesh>();
for (Spatial spatial : node.getChildren()) {
if (spatial instanceof Geometry) {
Mesh mesh = ((Geometry) spatial).getMesh();
if (mesh != null) {
meshes.add(mesh);
Material material = materialHelper.getParticlesMaterial(((Geometry) spatial).getMaterial(), alphaFunction, blenderContext);
emitter.setMaterial(material);// TODO: divide into several pieces
}
}
}
if (meshes.size() > 0 && emitterShape instanceof EmitterMeshVertexShape) {
((EmitterMeshVertexShape) emitterShape).setMeshes(meshes);
}
node.attachChild(emitter);
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Particles modifier is invalid! Cannot be applied to: {0}", node.getName());
} else {
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if(temporalMesh != null) {
temporalMesh.applyAfterMeshCreate(this);
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}
}

@ -0,0 +1,642 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.Edge;
import com.jme3.scene.plugins.blender.meshes.Face;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.textures.TexturePixel;
/**
* A modifier that subdivides the mesh using either simple or catmull-clark subdivision.
*
* @author Marcin Roguski (Kaelthas)
*/
public class SubdivisionSurfaceModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(SubdivisionSurfaceModifier.class.getName());
private static final int TYPE_CATMULLCLARK = 0;
private static final int TYPE_SIMPLE = 1;
private static final int FLAG_SUBDIVIDE_UVS = 0x8;
/** The subdivision type. */
private int subdivType;
/** The amount of subdivision levels. */
private int levels;
/** Indicates if the UV's should also be subdivided. */
private boolean subdivideUVS;
/** Stores the vertices that are located on original edges of the mesh. */
private Set<Integer> verticesOnOriginalEdges = new HashSet<Integer>();
/**
* Constructor loads all necessary modifier data.
* @param modifierStructure
* the modifier structure
* @param blenderContext
* the blender context
*/
public SubdivisionSurfaceModifier(Structure modifierStructure, BlenderContext blenderContext) {
if (this.validate(modifierStructure, blenderContext)) {
subdivType = ((Number) modifierStructure.getFieldValue("subdivType")).intValue();
levels = ((Number) modifierStructure.getFieldValue("levels")).intValue();
int flag = ((Number) modifierStructure.getFieldValue("flags")).intValue();
subdivideUVS = (flag & FLAG_SUBDIVIDE_UVS) != 0 && subdivType == TYPE_CATMULLCLARK;
if (subdivType != TYPE_CATMULLCLARK && subdivType != TYPE_SIMPLE) {
LOGGER.log(Level.SEVERE, "Unknown subdivision type: {0}.", subdivType);
invalid = true;
}
if (levels < 0) {
LOGGER.severe("The amount of subdivision levels cannot be negative.");
invalid = true;
}
}
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Subdivision surface modifier is invalid! Cannot be applied to: {0}", node.getName());
} else if (levels > 0) {// no need to do anything if the levels is set to zero
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if (temporalMesh != null) {
LOGGER.log(Level.FINE, "Applying subdivision surface modifier to: {0}", temporalMesh);
verticesOnOriginalEdges.clear();//in case the instance of this class was used more than once
for (Edge edge : temporalMesh.getEdges()) {
verticesOnOriginalEdges.add(edge.getFirstIndex());
verticesOnOriginalEdges.add(edge.getSecondIndex());
}
if (subdivType == TYPE_CATMULLCLARK) {
for (int i = 0; i < levels; ++i) {
this.subdivideSimple(temporalMesh);// first do simple subdivision ...
this.subdivideCatmullClark(temporalMesh);// ... and then apply Catmull-Clark algorithm
if (subdivideUVS) {// UV's can be subdivided only for Catmull-Clark subdivision algorithm
this.subdivideUVs(temporalMesh);
}
}
} else {
for (int i = 0; i < levels; ++i) {
this.subdivideSimple(temporalMesh);
}
}
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}
/**
* Catmull-Clark subdivision. It assumes that the mesh was already simple-subdivided.
* @param temporalMesh
* the mesh whose vertices will be transformed to form Catmull-Clark subdivision
*/
private void subdivideCatmullClark(TemporalMesh temporalMesh) {
Set<Integer> boundaryVertices = new HashSet<Integer>();
for (Edge edge : temporalMesh.getEdges()) {
if (!edge.isInFace()) {
boundaryVertices.add(edge.getFirstIndex());
boundaryVertices.add(edge.getSecondIndex());
} else {
if (temporalMesh.isBoundary(edge.getFirstIndex())) {
boundaryVertices.add(edge.getFirstIndex());
}
if (temporalMesh.isBoundary(edge.getSecondIndex())) {
boundaryVertices.add(edge.getSecondIndex());
}
}
}
List<CreasePoint> creasePoints = new ArrayList<CreasePoint>(temporalMesh.getVertexCount());
for (int i = 0; i < temporalMesh.getVertexCount(); ++i) {
// finding adjacent edges that were created by dividing original edges
List<Edge> adjacentOriginalEdges = new ArrayList<Edge>();
Collection<Edge> adjacentEdges = temporalMesh.getAdjacentEdges(i);
if(adjacentEdges != null) {// this can be null if a vertex with index 'i' is not connected to any face nor edge
for (Edge edge : temporalMesh.getAdjacentEdges(i)) {
if (verticesOnOriginalEdges.contains(edge.getFirstIndex()) || verticesOnOriginalEdges.contains(edge.getSecondIndex())) {
adjacentOriginalEdges.add(edge);
}
}
creasePoints.add(new CreasePoint(i, boundaryVertices.contains(i), adjacentOriginalEdges, temporalMesh));
} else {
creasePoints.add(null);//the count of crease points must be equal to vertex count; otherwise we'll get IndexOutofBoundsException later
}
}
Vector3f[] averageVert = new Vector3f[temporalMesh.getVertexCount()];
int[] averageCount = new int[temporalMesh.getVertexCount()];
for (Face face : temporalMesh.getFaces()) {
Vector3f centroid = face.computeCentroid();
for (Integer index : face.getIndexes()) {
if (boundaryVertices.contains(index)) {
Edge edge = this.findEdge(temporalMesh, index, face.getIndexes().getNextIndex(index));
if (temporalMesh.isBoundary(edge)) {
averageVert[index] = averageVert[index] == null ? edge.computeCentroid() : averageVert[index].addLocal(edge.computeCentroid());
averageCount[index] += 1;
}
edge = this.findEdge(temporalMesh, face.getIndexes().getPreviousIndex(index), index);
if (temporalMesh.isBoundary(edge)) {
averageVert[index] = averageVert[index] == null ? edge.computeCentroid() : averageVert[index].addLocal(edge.computeCentroid());
averageCount[index] += 1;
}
} else {
averageVert[index] = averageVert[index] == null ? centroid.clone() : averageVert[index].addLocal(centroid);
averageCount[index] += 1;
}
}
}
for (Edge edge : temporalMesh.getEdges()) {
if (!edge.isInFace()) {
Vector3f centroid = temporalMesh.getVertices().get(edge.getFirstIndex()).add(temporalMesh.getVertices().get(edge.getSecondIndex())).divideLocal(2);
averageVert[edge.getFirstIndex()] = averageVert[edge.getFirstIndex()] == null ? centroid.clone() : averageVert[edge.getFirstIndex()].addLocal(centroid);
averageVert[edge.getSecondIndex()] = averageVert[edge.getSecondIndex()] == null ? centroid.clone() : averageVert[edge.getSecondIndex()].addLocal(centroid);
averageCount[edge.getFirstIndex()] += 1;
averageCount[edge.getSecondIndex()] += 1;
}
}
for (int i = 0; i < averageVert.length; ++i) {
if(averageVert[i] != null && averageCount[i] > 0) {
Vector3f v = temporalMesh.getVertices().get(i);
averageVert[i].divideLocal(averageCount[i]);
// computing translation vector
Vector3f t = averageVert[i].subtract(v);
if (!boundaryVertices.contains(i)) {
t.multLocal(4 / (float) averageCount[i]);
}
// moving the vertex
v.addLocal(t);
// applying crease weight if necessary
CreasePoint creasePoint = creasePoints.get(i);
if (creasePoint.getTarget() != null && creasePoint.getWeight() != 0) {
t = creasePoint.getTarget().subtractLocal(v).multLocal(creasePoint.getWeight());
v.addLocal(t);
}
}
}
}
/**
* The method performs a simple subdivision of the mesh.
*
* @param temporalMesh
* the mesh to be subdivided
*/
private void subdivideSimple(TemporalMesh temporalMesh) {
Map<Edge, Integer> edgePoints = new HashMap<Edge, Integer>();
Map<Face, Integer> facePoints = new HashMap<Face, Integer>();
Set<Face> newFaces = new LinkedHashSet<Face>();
Set<Edge> newEdges = new LinkedHashSet<Edge>(temporalMesh.getEdges().size() * 4);
int originalFacesCount = temporalMesh.getFaces().size();
List<Map<String, Float>> vertexGroups = temporalMesh.getVertexGroups();
// the result vertex array will have verts in the following order [[original_verts], [face_verts], [edge_verts]]
List<Vector3f> vertices = temporalMesh.getVertices();
List<Vector3f> edgeVertices = new ArrayList<Vector3f>();
List<Vector3f> faceVertices = new ArrayList<Vector3f>();
// the same goes for normals
List<Vector3f> normals = temporalMesh.getNormals();
List<Vector3f> edgeNormals = new ArrayList<Vector3f>();
List<Vector3f> faceNormals = new ArrayList<Vector3f>();
List<Face> faces = temporalMesh.getFaces();
for (Face face : faces) {
Map<String, List<Vector2f>> uvSets = face.getUvSets();
Vector3f facePoint = face.computeCentroid();
Integer facePointIndex = vertices.size() + faceVertices.size();
facePoints.put(face, facePointIndex);
faceVertices.add(facePoint);
faceNormals.add(this.computeFaceNormal(face));
Map<String, Vector2f> faceUV = this.computeFaceUVs(face);
byte[] faceVertexColor = this.computeFaceVertexColor(face);
Map<String, Float> faceVertexGroups = this.computeFaceVertexGroups(face);
if (vertexGroups.size() > 0) {
vertexGroups.add(faceVertexGroups);
}
for (int i = 0; i < face.getIndexes().size(); ++i) {
int vIndex = face.getIndexes().get(i);
int vPrevIndex = i == 0 ? face.getIndexes().get(face.getIndexes().size() - 1) : face.getIndexes().get(i - 1);
int vNextIndex = i == face.getIndexes().size() - 1 ? face.getIndexes().get(0) : face.getIndexes().get(i + 1);
Edge prevEdge = this.findEdge(temporalMesh, vPrevIndex, vIndex);
Edge nextEdge = this.findEdge(temporalMesh, vIndex, vNextIndex);
int vPrevEdgeVertIndex = edgePoints.containsKey(prevEdge) ? edgePoints.get(prevEdge) : -1;
int vNextEdgeVertIndex = edgePoints.containsKey(nextEdge) ? edgePoints.get(nextEdge) : -1;
Vector3f v = temporalMesh.getVertices().get(vIndex);
if (vPrevEdgeVertIndex < 0) {
vPrevEdgeVertIndex = vertices.size() + originalFacesCount + edgeVertices.size();
verticesOnOriginalEdges.add(vPrevEdgeVertIndex);
edgeVertices.add(vertices.get(vPrevIndex).add(v).divideLocal(2));
edgeNormals.add(normals.get(vPrevIndex).add(normals.get(vIndex)).normalizeLocal());
edgePoints.put(prevEdge, vPrevEdgeVertIndex);
if (vertexGroups.size() > 0) {
vertexGroups.add(this.interpolateVertexGroups(Arrays.asList(vertexGroups.get(vPrevIndex), vertexGroups.get(vIndex))));
}
}
if (vNextEdgeVertIndex < 0) {
vNextEdgeVertIndex = vertices.size() + originalFacesCount + edgeVertices.size();
verticesOnOriginalEdges.add(vNextEdgeVertIndex);
edgeVertices.add(vertices.get(vNextIndex).add(v).divideLocal(2));
edgeNormals.add(normals.get(vNextIndex).add(normals.get(vIndex)).normalizeLocal());
edgePoints.put(nextEdge, vNextEdgeVertIndex);
if (vertexGroups.size() > 0) {
vertexGroups.add(this.interpolateVertexGroups(Arrays.asList(vertexGroups.get(vNextIndex), vertexGroups.get(vIndex))));
}
}
Integer[] indexes = new Integer[] { vIndex, vNextEdgeVertIndex, facePointIndex, vPrevEdgeVertIndex };
Map<String, List<Vector2f>> newUVSets = null;
if (uvSets != null) {
newUVSets = new HashMap<String, List<Vector2f>>(uvSets.size());
for (Entry<String, List<Vector2f>> uvset : uvSets.entrySet()) {
int indexOfvIndex = i;
int indexOfvPrevIndex = face.getIndexes().indexOf(vPrevIndex);
int indexOfvNextIndex = face.getIndexes().indexOf(vNextIndex);
Vector2f uv1 = uvset.getValue().get(indexOfvIndex);
Vector2f uv2 = uvset.getValue().get(indexOfvNextIndex).add(uv1).divideLocal(2);
Vector2f uv3 = faceUV.get(uvset.getKey());
Vector2f uv4 = uvset.getValue().get(indexOfvPrevIndex).add(uv1).divideLocal(2);
List<Vector2f> uvList = Arrays.asList(uv1, uv2, uv3, uv4);
newUVSets.put(uvset.getKey(), new ArrayList<Vector2f>(uvList));
}
}
List<byte[]> vertexColors = null;
if (face.getVertexColors() != null) {
int indexOfvIndex = i;
int indexOfvPrevIndex = face.getIndexes().indexOf(vPrevIndex);
int indexOfvNextIndex = face.getIndexes().indexOf(vNextIndex);
byte[] vCol1 = face.getVertexColors().get(indexOfvIndex);
byte[] vCol2 = this.interpolateVertexColors(face.getVertexColors().get(indexOfvNextIndex), vCol1);
byte[] vCol3 = faceVertexColor;
byte[] vCol4 = this.interpolateVertexColors(face.getVertexColors().get(indexOfvPrevIndex), vCol1);
vertexColors = new ArrayList<byte[]>(Arrays.asList(vCol1, vCol2, vCol3, vCol4));
}
newFaces.add(new Face(indexes, face.isSmooth(), face.getMaterialNumber(), newUVSets, vertexColors, temporalMesh));
newEdges.add(new Edge(vIndex, vNextEdgeVertIndex, nextEdge.getCrease(), true, temporalMesh));
newEdges.add(new Edge(vNextEdgeVertIndex, facePointIndex, 0, true, temporalMesh));
newEdges.add(new Edge(facePointIndex, vPrevEdgeVertIndex, 0, true, temporalMesh));
newEdges.add(new Edge(vPrevEdgeVertIndex, vIndex, prevEdge.getCrease(), true, temporalMesh));
}
}
vertices.addAll(faceVertices);
vertices.addAll(edgeVertices);
normals.addAll(faceNormals);
normals.addAll(edgeNormals);
for (Edge edge : temporalMesh.getEdges()) {
if (!edge.isInFace()) {
int newVertexIndex = vertices.size();
vertices.add(vertices.get(edge.getFirstIndex()).add(vertices.get(edge.getSecondIndex())).divideLocal(2));
normals.add(normals.get(edge.getFirstIndex()).add(normals.get(edge.getSecondIndex())).normalizeLocal());
newEdges.add(new Edge(edge.getFirstIndex(), newVertexIndex, edge.getCrease(), false, temporalMesh));
newEdges.add(new Edge(newVertexIndex, edge.getSecondIndex(), edge.getCrease(), false, temporalMesh));
verticesOnOriginalEdges.add(newVertexIndex);
}
}
temporalMesh.getFaces().clear();
temporalMesh.getFaces().addAll(newFaces);
temporalMesh.getEdges().clear();
temporalMesh.getEdges().addAll(newEdges);
temporalMesh.rebuildIndexesMappings();
}
/**
* The method subdivides mesh's UV coordinates. It actually performs only Catmull-Clark modifications because if any UV's are present then they are
* automatically subdivided by the simple algorithm.
* @param temporalMesh
* the mesh whose UV coordinates will be applied Catmull-Clark algorithm
*/
private void subdivideUVs(TemporalMesh temporalMesh) {
List<Face> faces = temporalMesh.getFaces();
Map<String, UvCoordsSubdivideTemporalMesh> subdividedUVS = new HashMap<String, UvCoordsSubdivideTemporalMesh>();
for (Face face : faces) {
if (face.getUvSets() != null) {
for (Entry<String, List<Vector2f>> uvset : face.getUvSets().entrySet()) {
UvCoordsSubdivideTemporalMesh uvCoordsSubdivideTemporalMesh = subdividedUVS.get(uvset.getKey());
if (uvCoordsSubdivideTemporalMesh == null) {
try {
uvCoordsSubdivideTemporalMesh = new UvCoordsSubdivideTemporalMesh(temporalMesh.getBlenderContext());
} catch (BlenderFileException e) {
assert false : "Something went really wrong! The UvCoordsSubdivideTemporalMesh class should NOT throw exceptions here!";
}
subdividedUVS.put(uvset.getKey(), uvCoordsSubdivideTemporalMesh);
}
uvCoordsSubdivideTemporalMesh.addFace(uvset.getValue());
}
}
}
for (Entry<String, UvCoordsSubdivideTemporalMesh> entry : subdividedUVS.entrySet()) {
entry.getValue().rebuildIndexesMappings();
this.subdivideCatmullClark(entry.getValue());
for (int i = 0; i < faces.size(); ++i) {
List<Vector2f> uvs = faces.get(i).getUvSets().get(entry.getKey());
if (uvs != null) {
uvs.clear();
uvs.addAll(entry.getValue().faceToUVs(i));
}
}
}
}
/**
* The method computes the face's normal vector.
* @param face
* the face of the mesh
* @return face's normal vector
*/
private Vector3f computeFaceNormal(Face face) {
Vector3f result = new Vector3f();
for (Integer index : face.getIndexes()) {
result.addLocal(face.getTemporalMesh().getNormals().get(index));
}
result.divideLocal(face.getIndexes().size());
return result;
}
/**
* The method computes the UV coordinates of the face middle point.
* @param face
* the face of the mesh
* @return a map whose key is the name of the UV set and value is the UV coordinate of the face's middle point
*/
private Map<String, Vector2f> computeFaceUVs(Face face) {
Map<String, Vector2f> result = null;
Map<String, List<Vector2f>> uvSets = face.getUvSets();
if (uvSets != null && uvSets.size() > 0) {
result = new HashMap<String, Vector2f>(uvSets.size());
for (Entry<String, List<Vector2f>> entry : uvSets.entrySet()) {
Vector2f faceUV = new Vector2f();
for (Vector2f uv : entry.getValue()) {
faceUV.addLocal(uv);
}
faceUV.divideLocal(entry.getValue().size());
result.put(entry.getKey(), faceUV);
}
}
return result;
}
/**
* The mesh interpolates the values of vertex groups weights for new vertices.
* @param vertexGroups
* the vertex groups
* @return interpolated weights of given vertex groups' weights
*/
private Map<String, Float> interpolateVertexGroups(List<Map<String, Float>> vertexGroups) {
Map<String, Float> weightSums = new HashMap<String, Float>();
if (vertexGroups.size() > 0) {
for (Map<String, Float> vGroup : vertexGroups) {
for (Entry<String, Float> entry : vGroup.entrySet()) {
if (weightSums.containsKey(entry.getKey())) {
weightSums.put(entry.getKey(), weightSums.get(entry.getKey()) + entry.getValue());
} else {
weightSums.put(entry.getKey(), entry.getValue());
}
}
}
}
Map<String, Float> result = new HashMap<String, Float>(weightSums.size());
for (Entry<String, Float> entry : weightSums.entrySet()) {
result.put(entry.getKey(), entry.getValue() / vertexGroups.size());
}
return result;
}
/**
* The method computes the vertex groups values for face's middle point.
* @param face
* the face of the mesh
* @return face's middle point interpolated vertex groups' weights
*/
private Map<String, Float> computeFaceVertexGroups(Face face) {
if (face.getTemporalMesh().getVertexGroups().size() > 0) {
List<Map<String, Float>> vertexGroups = new ArrayList<Map<String, Float>>(face.getIndexes().size());
for (Integer index : face.getIndexes()) {
vertexGroups.add(face.getTemporalMesh().getVertexGroups().get(index));
}
return this.interpolateVertexGroups(vertexGroups);
}
return new HashMap<String, Float>();
}
/**
* The method computes face's middle point vertex color.
* @param face
* the face of the mesh
* @return face's middle point vertex color
*/
private byte[] computeFaceVertexColor(Face face) {
if (face.getVertexColors() != null) {
return this.interpolateVertexColors(face.getVertexColors().toArray(new byte[face.getVertexColors().size()][]));
}
return null;
}
/**
* The method computes the average value for the given vertex colors.
* @param colors
* the vertex colors
* @return vertex colors' average value
*/
private byte[] interpolateVertexColors(byte[]... colors) {
TexturePixel pixel = new TexturePixel();
TexturePixel temp = new TexturePixel();
for (int i = 0; i < colors.length; ++i) {
temp.fromARGB8(colors[i][3], colors[i][0], colors[i][1], colors[i][2]);
pixel.add(temp);
}
pixel.divide(colors.length);
byte[] result = new byte[4];
pixel.toRGBA8(result);
return result;
}
/**
* The method finds an edge between the given vertices in the mesh.
* @param temporalMesh
* the mesh
* @param index1
* first index of the edge
* @param index2
* second index of the edge
* @return found edge or null
*/
private Edge findEdge(TemporalMesh temporalMesh, int index1, int index2) {
for (Edge edge : temporalMesh.getEdges()) {
if (edge.getFirstIndex() == index1 && edge.getSecondIndex() == index2 || edge.getFirstIndex() == index2 && edge.getSecondIndex() == index1) {
return edge;
}
}
return null;
}
/**
* This is a helper class for UV coordinates subdivision. UV's form a mesh that is being applied the same algorithms as a regular mesh.
* This way one code handles two issues. After applying Catmull-Clark algorithm the UV-mesh is transformed back into UV coordinates.
*
* @author Marcin Roguski (Kaelthas)
*/
private static class UvCoordsSubdivideTemporalMesh extends TemporalMesh {
private static final Vector3f NORMAL = new Vector3f(0, 0, 1);
public UvCoordsSubdivideTemporalMesh(BlenderContext blenderContext) throws BlenderFileException {
super(null, blenderContext, false);
}
/**
* Adds a UV-face to the mesh.
* @param uvs
* the UV coordinates
*/
public void addFace(List<Vector2f> uvs) {
Integer[] indexes = new Integer[uvs.size()];
int i = 0;
for (Vector2f uv : uvs) {
Vector3f v = new Vector3f(uv.x, uv.y, 0);
int index = vertices.indexOf(v);
if (index >= 0) {
indexes[i++] = index;
} else {
indexes[i++] = vertices.size();
vertices.add(v);
normals.add(NORMAL);
}
}
faces.add(new Face(indexes, false, 0, null, null, this));
for (i = 1; i < indexes.length; ++i) {
edges.add(new Edge(indexes[i - 1], indexes[i], 0, true, this));
}
edges.add(new Edge(indexes[indexes.length - 1], indexes[0], 0, true, this));
}
/**
* Converts the mesh back into UV coordinates for the given face.
* @param faceIndex
* the index of the face
* @return UV coordinates
*/
public List<Vector2f> faceToUVs(int faceIndex) {
Face face = faces.get(faceIndex);
List<Vector2f> result = new ArrayList<Vector2f>(face.getIndexes().size());
for (Integer index : face.getIndexes()) {
Vector3f v = vertices.get(index);
result.add(new Vector2f(v.x, v.y));
}
return result;
}
}
/**
* A point computed for each vertex before applying CC subdivision and after simple subdivision.
* This class has a target where the vertices will be drawn to with a proper strength (value from 0 to 1).
*
* The algorithm of computing the target point was made by observing how blender behaves.
* If a vertex has one or less creased edges (means edges that have non zero crease value) the target will not exist.
* If a vertex is a border vertex and has two creased edges - the target will be the original simple subdivided vertex.
* If a vertex is not a border vertex and have two creased edges - then it will be drawned to the plane defined by those
* two edges.
* If a vertex has 3 or more creased edges it will be drawn to its original vertex before CC subdivision with average strength
* computed from edges' crease values.
*
* @author Marcin Roguski (Kaelthas)
*/
private static class CreasePoint {
private Vector3f target = new Vector3f();
private float weight;
private int index;
public CreasePoint(int index, boolean borderIndex, List<Edge> creaseEdges, TemporalMesh temporalMesh) {
this.index = index;
if (creaseEdges == null || creaseEdges.size() <= 1) {
target = null;// crease is used when vertex belongs to at least 2 creased edges
} else {
int creasedEdgesCount = 0;
for (Edge edge : creaseEdges) {
if (edge.getCrease() > 0) {
++creasedEdgesCount;
weight += edge.getCrease();
target.addLocal(temporalMesh.getVertices().get(edge.getOtherIndex(index)));
}
}
if (creasedEdgesCount <= 1) {
target = null;// crease is used when vertex belongs to at least 2 creased edges
} else if (creasedEdgesCount == 2) {
if (borderIndex) {
target.set(temporalMesh.getVertices().get(index));
} else {
target.addLocal(temporalMesh.getVertices().get(index)).divideLocal(creasedEdgesCount + 1);
}
} else {
target.set(temporalMesh.getVertices().get(index));
}
if (creasedEdgesCount > 0) {
weight /= creasedEdgesCount;
}
}
}
public Vector3f getTarget() {
return target;
}
public float getWeight() {
return weight;
}
@Override
public String toString() {
return "CreasePoint [index = " + index + ", target=" + target + ", weight=" + weight + "]";
}
}
}

@ -0,0 +1,54 @@
package com.jme3.scene.plugins.blender.modifiers;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
/**
* The triangulation modifier. It does not take any settings into account so if the result is different than
* in blender then please apply the modifier before importing.
*
* @author Marcin Roguski
*/
public class TriangulateModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(TriangulateModifier.class.getName());
/**
* This constructor reads animation data from the object structore. The
* stored data is the AnimData and additional data is armature's OMA.
*
* @param objectStructure
* the structure of the object
* @param modifierStructure
* the structure of the modifier
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public TriangulateModifier(Structure objectStructure, Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
if (this.validate(modifierStructure, blenderContext)) {
LOGGER.warning("Triangulation modifier does not take modifier options into account. If triangulation result is different" + " than the model in blender please apply the modifier before importing!");
}
}
@Override
public void apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Triangulate modifier is invalid! Cannot be applied to: {0}", node.getName());
}
TemporalMesh temporalMesh = this.getTemporalMesh(node);
if (temporalMesh != null) {
LOGGER.log(Level.FINE, "Applying triangulation modifier to: {0}", temporalMesh);
temporalMesh.triangulate();
} else {
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node);
}
}
}

@ -0,0 +1,520 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.objects;
import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.light.Light;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.LightNode;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.animations.AnimationHelper;
import com.jme3.scene.plugins.blender.cameras.CameraHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.curves.CurvesHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.lights.LightHelper;
import com.jme3.scene.plugins.blender.meshes.MeshHelper;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.modifiers.Modifier;
import com.jme3.scene.plugins.blender.modifiers.ModifierHelper;
import com.jme3.util.TempVars;
/**
* A class that is used in object calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ObjectHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(ObjectHelper.class.getName());
public static final String OMA_MARKER = "oma";
public static final String ARMATURE_NODE_MARKER = "armature-node";
/**
* This constructor parses the given blender version and stores the result.
* Some functionalities may differ in different blender versions.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public ObjectHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This method reads the given structure and createn an object that
* represents the data.
*
* @param objectStructure
* the object's structure
* @param blenderContext
* the blender context
* @return blener's object representation or null if its type is excluded from loading
* @throws BlenderFileException
* an exception is thrown when the given data is inapropriate
*/
public Object toObject(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException {
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();
ObjectType objectType = ObjectType.valueOf(type);
LOGGER.log(Level.FINE, "Type of the object: {0}.", objectType);
int lay = ((Number) objectStructure.getFieldValue("lay")).intValue();
if ((lay & blenderContext.getBlenderKey().getLayersToLoad()) == 0) {
LOGGER.fine("The layer this object is located in is not included in loading.");
return null;
}
blenderContext.pushParent(objectStructure);
String name = objectStructure.getName();
LOGGER.log(Level.FINE, "Loading obejct: {0}", name);
int restrictflag = ((Number) objectStructure.getFieldValue("restrictflag")).intValue();
boolean visible = (restrictflag & 0x01) != 0;
Pointer pParent = (Pointer) objectStructure.getFieldValue("parent");
Object parent = blenderContext.getLoadedFeature(pParent.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (parent == null && pParent.isNotNull()) {
Structure parentStructure = pParent.fetchData().get(0);
parent = this.toObject(parentStructure, blenderContext);
}
Transform t = this.getTransformation(objectStructure, blenderContext);
LOGGER.log(Level.FINE, "Importing object of type: {0}", objectType);
Node result = null;
try {
switch (objectType) {
case LATTICE:
case METABALL:
case TEXT:
case WAVE:
LOGGER.log(Level.WARNING, "{0} type is not supported but the node will be returned in order to keep parent - child relationship.", objectType);
case EMPTY:
case ARMATURE:
// need to use an empty node to properly create
// parent-children relationships between nodes
result = new Node(name);
break;
case MESH:
result = new Node(name);
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
Pointer pMesh = (Pointer) objectStructure.getFieldValue("data");
List<Structure> meshesArray = pMesh.fetchData();
TemporalMesh temporalMesh = meshHelper.toTemporalMesh(meshesArray.get(0), blenderContext);
if (temporalMesh != null) {
result.attachChild(temporalMesh);
}
break;
case SURF:
case CURVE:
result = new Node(name);
Pointer pCurve = (Pointer) objectStructure.getFieldValue("data");
if (pCurve.isNotNull()) {
CurvesHelper curvesHelper = blenderContext.getHelper(CurvesHelper.class);
Structure curveData = pCurve.fetchData().get(0);
TemporalMesh curvesTemporalMesh = curvesHelper.toCurve(curveData, blenderContext);
if (curvesTemporalMesh != null) {
result.attachChild(curvesTemporalMesh);
}
}
break;
case LAMP:
Pointer pLamp = (Pointer) objectStructure.getFieldValue("data");
if (pLamp.isNotNull()) {
LightHelper lightHelper = blenderContext.getHelper(LightHelper.class);
List<Structure> lampsArray = pLamp.fetchData();
Light light = lightHelper.toLight(lampsArray.get(0), blenderContext);
if (light == null) {
// 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);
} else {
result = new LightNode(name, light);
}
}
break;
case CAMERA:
Pointer pCamera = (Pointer) objectStructure.getFieldValue("data");
if (pCamera.isNotNull()) {
CameraHelper cameraHelper = blenderContext.getHelper(CameraHelper.class);
List<Structure> camerasArray = pCamera.fetchData();
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;
default:
LOGGER.log(Level.WARNING, "Unsupported object type: {0}", type);
}
if (result != null) {
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();
blenderContext.addLoadedFeatures(oma, LoadedDataType.STRUCTURE, objectStructure);
blenderContext.addLoadedFeatures(oma, LoadedDataType.FEATURE, result);
blenderContext.addMarker(OMA_MARKER, result, objectStructure.getOldMemoryAddress());
if (objectType == ObjectType.ARMATURE) {
blenderContext.addMarker(ARMATURE_NODE_MARKER, result, Boolean.TRUE);
}
result.setLocalTransform(t);
result.setCullHint(visible ? CullHint.Always : CullHint.Inherit);
if (parent instanceof Node) {
((Node) parent).attachChild(result);
}
LOGGER.fine("Reading and applying object's modifiers.");
ModifierHelper modifierHelper = blenderContext.getHelper(ModifierHelper.class);
Collection<Modifier> modifiers = modifierHelper.readModifiers(objectStructure, blenderContext);
for (Modifier modifier : modifiers) {
modifier.apply(result, blenderContext);
}
if (result.getChildren() != null && result.getChildren().size() > 0) {
if (result.getChildren().size() == 1 && result.getChild(0) instanceof TemporalMesh) {
LOGGER.fine("Converting temporal mesh into jme geometries.");
((TemporalMesh) result.getChild(0)).toGeometries();
}
LOGGER.fine("Applying proper scale to the geometries.");
for (Spatial child : result.getChildren()) {
if (child instanceof Geometry) {
this.flipMeshIfRequired((Geometry) child, child.getWorldScale());
}
}
}
// I prefer do compute bounding box here than read it from the file
result.updateModelBound();
LOGGER.fine("Applying animations to the object if such are defined.");
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
animationHelper.applyAnimations(result, blenderContext.getBlenderKey().getAnimationMatchMethod());
LOGGER.fine("Loading constraints connected with this object.");
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
constraintHelper.loadConstraints(objectStructure, blenderContext);
LOGGER.fine("Loading custom properties.");
if (blenderContext.getBlenderKey().isLoadObjectProperties()) {
Properties properties = this.loadProperties(objectStructure, blenderContext);
// the loaded property is a group property, so we need to get
// each value and set it to Spatial
if (properties != null && properties.getValue() != null) {
this.applyProperties(result, properties);
}
}
}
} finally {
blenderContext.popParent();
}
return result;
}
/**
* The method flips the mesh if the scale is mirroring it. Mirroring scale has either 1 or all 3 factors negative.
* If two factors are negative then there is no mirroring because a rotation and translation can be found that will
* lead to the same transform when all scales are positive.
*
* @param geometry
* the geometry that is being flipped if necessary
* @param scale
* the scale vector of the given geometry
*/
private void flipMeshIfRequired(Geometry geometry, Vector3f scale) {
float s = scale.x * scale.y * scale.z;
if (s < 0 && geometry.getMesh() != null) {// negative s means that the scale is mirroring the object
FloatBuffer normals = geometry.getMesh().getFloatBuffer(Type.Normal);
if (normals != null) {
for (int i = 0; i < normals.limit(); i += 3) {
if (scale.x < 0) {
normals.put(i, -normals.get(i));
}
if (scale.y < 0) {
normals.put(i + 1, -normals.get(i + 1));
}
if (scale.z < 0) {
normals.put(i + 2, -normals.get(i + 2));
}
}
}
if (geometry.getMesh().getMode() == Mode.Triangles) {// there is no need to flip the indexes for lines and points
LOGGER.finer("Flipping index order in triangle mesh.");
Buffer indexBuffer = geometry.getMesh().getBuffer(Type.Index).getData();
for (int i = 0; i < indexBuffer.limit(); i += 3) {
if (indexBuffer instanceof ShortBuffer) {
short index = ((ShortBuffer) indexBuffer).get(i + 1);
((ShortBuffer) indexBuffer).put(i + 1, ((ShortBuffer) indexBuffer).get(i + 2));
((ShortBuffer) indexBuffer).put(i + 2, index);
} else {
int index = ((IntBuffer) indexBuffer).get(i + 1);
((IntBuffer) indexBuffer).put(i + 1, ((IntBuffer) indexBuffer).get(i + 2));
((IntBuffer) indexBuffer).put(i + 2, index);
}
}
}
}
}
/**
* Checks if the first given OMA points to a parent of the second one.
* The parent need not to be the direct one. This method should be called when we are sure
* that both of the features are alred loaded because it does not check it.
* The OMA's should point to a spatials, otherwise the function will throw ClassCastException.
* @param supposedParentOMA
* the OMA of the node that we suppose might be a parent of the second one
* @param spatialOMA
* the OMA of the scene's node
* @return <b>true</b> if the first given OMA points to a parent of the second one and <b>false</b> otherwise
*/
public boolean isParent(Long supposedParentOMA, Long spatialOMA) {
Spatial supposedParent = (Spatial) blenderContext.getLoadedFeature(supposedParentOMA, LoadedDataType.FEATURE);
Spatial spatial = (Spatial) blenderContext.getLoadedFeature(spatialOMA, LoadedDataType.FEATURE);
Spatial parent = spatial.getParent();
while (parent != null) {
if (parent.equals(supposedParent)) {
return true;
}
parent = parent.getParent();
}
return false;
}
/**
* This method calculates local transformation for the object. Parentage is
* taken under consideration.
*
* @param objectStructure
* the object's structure
* @return objects transformation relative to its parent
*/
public Transform getTransformation(Structure objectStructure, BlenderContext blenderContext) {
TempVars tempVars = TempVars.get();
Matrix4f parentInv = tempVars.tempMat4;
Pointer pParent = (Pointer) objectStructure.getFieldValue("parent");
if (pParent.isNotNull()) {
Structure parentObjectStructure = (Structure) blenderContext.getLoadedFeature(pParent.getOldMemoryAddress(), LoadedDataType.STRUCTURE);
this.getMatrix(parentObjectStructure, "obmat", fixUpAxis, parentInv).invertLocal();
} else {
parentInv.loadIdentity();
}
Matrix4f globalMatrix = this.getMatrix(objectStructure, "obmat", fixUpAxis, tempVars.tempMat42);
Matrix4f localMatrix = parentInv.multLocal(globalMatrix);
this.getSizeSignums(objectStructure, tempVars.vect1);
localMatrix.toTranslationVector(tempVars.vect2);
localMatrix.toRotationQuat(tempVars.quat1);
localMatrix.toScaleVector(tempVars.vect3);
Transform t = new Transform(tempVars.vect2, tempVars.quat1.normalizeLocal(), tempVars.vect3.multLocal(tempVars.vect1));
tempVars.release();
return t;
}
/**
* The method gets the signs of the scale factors and stores them properly in the given vector.
* @param objectStructure
* the object's structure
* @param store
* the vector where the result will be stored
*/
@SuppressWarnings("unchecked")
private void getSizeSignums(Structure objectStructure, Vector3f store) {
DynamicArray<Number> size = (DynamicArray<Number>) objectStructure.getFieldValue("size");
if (fixUpAxis) {
store.x = Math.signum(size.get(0).floatValue());
store.y = Math.signum(size.get(2).floatValue());
store.z = Math.signum(size.get(1).floatValue());
} else {
store.x = Math.signum(size.get(0).floatValue());
store.y = Math.signum(size.get(1).floatValue());
store.z = Math.signum(size.get(2).floatValue());
}
}
/**
* This method returns the matrix of a given name for the given structure.
* It takes up axis into consideration.
*
* The method that moves the matrix from Z-up axis to Y-up axis space is as follows:
* - load the matrix directly from blender (it has the Z-up axis orientation)
* - switch the second and third rows in the matrix
* - switch the second and third column in the matrix
* - multiply the values in the third row by -1
* - multiply the values in the third column by -1
*
* The result matrix is now in Y-up axis orientation.
* The procedure was discovered by experimenting but it looks like it's working :)
* The previous procedure transformet the loaded matrix into component (loc, rot, scale),
* switched several values and pu the back into the matrix.
* It worked fine until models with negative scale are used.
* The current method is not touched by that flaw.
*
* @param structure
* the structure with matrix data
* @param matrixName
* the name of the matrix
* @param fixUpAxis
* tells if the Y axis is a UP axis
* @param store
* the matrix where the result will pe placed
* @return the required matrix
*/
@SuppressWarnings("unchecked")
private Matrix4f getMatrix(Structure structure, String matrixName, boolean fixUpAxis, Matrix4f store) {
DynamicArray<Number> obmat = (DynamicArray<Number>) structure.getFieldValue(matrixName);
// the matrix must be square
int rowAndColumnSize = Math.abs((int) Math.sqrt(obmat.getTotalSize()));
for (int i = 0; i < rowAndColumnSize; ++i) {
for (int j = 0; j < rowAndColumnSize; ++j) {
float value = obmat.get(j, i).floatValue();
if (Math.abs(value) <= FastMath.FLT_EPSILON) {
value = 0;
}
store.set(i, j, value);
}
}
if (fixUpAxis) {
// first switch the second and third row
for (int i = 0; i < 4; ++i) {
float temp = store.get(1, i);
store.set(1, i, store.get(2, i));
store.set(2, i, temp);
}
// then switch the second and third column
for (int i = 0; i < 4; ++i) {
float temp = store.get(i, 1);
store.set(i, 1, store.get(i, 2));
store.set(i, 2, temp);
}
// multiply the values in the third row by -1
store.m20 *= -1;
store.m21 *= -1;
store.m22 *= -1;
store.m23 *= -1;
// multiply the values in the third column by -1
store.m02 *= -1;
store.m12 *= -1;
store.m22 *= -1;
store.m32 *= -1;
}
return store;
}
/**
* This method returns the matrix of a given name for the given structure.
* It takes up axis into consideration.
*
* @param structure
* the structure with matrix data
* @param matrixName
* the name of the matrix
* @param fixUpAxis
* tells if the Y axis is a UP axis
* @return the required matrix
*/
public Matrix4f getMatrix(Structure structure, String matrixName, boolean fixUpAxis) {
return this.getMatrix(structure, matrixName, fixUpAxis, new Matrix4f());
}
private static enum ObjectType {
EMPTY(0), MESH(1), CURVE(2), SURF(3), TEXT(4), METABALL(5), LAMP(10), CAMERA(11), WAVE(21), LATTICE(22), ARMATURE(25);
private int blenderTypeValue;
private ObjectType(int blenderTypeValue) {
this.blenderTypeValue = blenderTypeValue;
}
public static ObjectType valueOf(int blenderTypeValue) throws BlenderFileException {
for (ObjectType type : ObjectType.values()) {
if (type.blenderTypeValue == blenderTypeValue) {
return type;
}
}
throw new BlenderFileException("Unknown type value: " + blenderTypeValue);
}
}
}

@ -0,0 +1,365 @@
package com.jme3.scene.plugins.blender.objects;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The blender object's custom properties.
* This class is valid for all versions of blender.
* @author Marcin Roguski (Kaelthas)
*/
public class Properties implements Cloneable {
// property type
public static final int IDP_STRING = 0;
public static final int IDP_INT = 1;
public static final int IDP_FLOAT = 2;
public static final int IDP_ARRAY = 5;
public static final int IDP_GROUP = 6;
// public static final int IDP_ID = 7;//this is not implemented in blender (yet)
public static final int IDP_DOUBLE = 8;
// the following are valid for blender 2.5x+
public static final int IDP_IDPARRAY = 9;
public static final int IDP_NUMTYPES = 10;
protected static final String RNA_PROPERTY_NAME = "_RNA_UI";
/** Default name of the property (used if the name is not specified in blender file). */
protected static final String DEFAULT_NAME = "Unnamed property";
/** The name of the property. */
private String name;
/** The type of the property. */
private int type;
/** The subtype of the property. Defines the type of array's elements. */
private int subType;
/** The value of the property. */
private Object value;
/** The description of the property. */
private String description;
/**
* This method loads the property from the belnder file.
* @param idPropertyStructure
* the ID structure constining the property
* @param blenderContext
* the blender context
* @throws BlenderFileException
* an exception is thrown when the belnder file is somehow invalid
*/
public void load(Structure idPropertyStructure, BlenderContext blenderContext) throws BlenderFileException {
name = idPropertyStructure.getFieldValue("name").toString();
if (name == null || name.length() == 0) {
name = DEFAULT_NAME;
}
subType = ((Number) idPropertyStructure.getFieldValue("subtype")).intValue();
type = ((Number) idPropertyStructure.getFieldValue("type")).intValue();
// reading the data
Structure data = (Structure) idPropertyStructure.getFieldValue("data");
int len = ((Number) idPropertyStructure.getFieldValue("len")).intValue();
switch (type) {
case IDP_STRING: {
Pointer pointer = (Pointer) data.getFieldValue("pointer");
BlenderInputStream bis = blenderContext.getInputStream();
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pointer.getOldMemoryAddress());
bis.setPosition(dataFileBlock.getBlockPosition());
value = bis.readString();
break;
}
case IDP_INT:
int intValue = ((Number) data.getFieldValue("val")).intValue();
value = Integer.valueOf(intValue);
break;
case IDP_FLOAT:
int floatValue = ((Number) data.getFieldValue("val")).intValue();
value = Float.valueOf(Float.intBitsToFloat(floatValue));
break;
case IDP_ARRAY: {
Pointer pointer = (Pointer) data.getFieldValue("pointer");
BlenderInputStream bis = blenderContext.getInputStream();
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pointer.getOldMemoryAddress());
bis.setPosition(dataFileBlock.getBlockPosition());
int elementAmount = dataFileBlock.getSize();
switch (subType) {
case IDP_INT:
elementAmount /= 4;
int[] intList = new int[elementAmount];
for (int i = 0; i < elementAmount; ++i) {
intList[i] = bis.readInt();
}
value = intList;
break;
case IDP_FLOAT:
elementAmount /= 4;
float[] floatList = new float[elementAmount];
for (int i = 0; i < elementAmount; ++i) {
floatList[i] = bis.readFloat();
}
value = floatList;
break;
case IDP_DOUBLE:
elementAmount /= 8;
double[] doubleList = new double[elementAmount];
for (int i = 0; i < elementAmount; ++i) {
doubleList[i] = bis.readDouble();
}
value = doubleList;
break;
default:
throw new IllegalStateException("Invalid array subtype: " + subType);
}
}
case IDP_GROUP:
Structure group = (Structure) data.getFieldValue("group");
List<Structure> dataList = group.evaluateListBase();
List<Properties> subProperties = new ArrayList<Properties>(len);
for (Structure d : dataList) {
Properties properties = new Properties();
properties.load(d, blenderContext);
subProperties.add(properties);
}
value = subProperties;
break;
case IDP_DOUBLE:
int doublePart1 = ((Number) data.getFieldValue("val")).intValue();
int doublePart2 = ((Number) data.getFieldValue("val2")).intValue();
long doubleVal = (long) doublePart2 << 32 | doublePart1;
value = Double.valueOf(Double.longBitsToDouble(doubleVal));
break;
case IDP_IDPARRAY: {
Pointer pointer = (Pointer) data.getFieldValue("pointer");
List<Structure> arrays = pointer.fetchData();
List<Object> result = new ArrayList<Object>(arrays.size());
Properties temp = new Properties();
for (Structure array : arrays) {
temp.load(array, blenderContext);
result.add(temp.value);
}
value = result;
break;
}
case IDP_NUMTYPES:
throw new UnsupportedOperationException();
// case IDP_ID://not yet implemented in blender
// return null;
default:
throw new IllegalStateException("Unknown custom property type: " + type);
}
this.completeLoading();
}
/**
* This method returns the name of the property.
* @return the name of the property
*/
public String getName() {
return name;
}
/**
* This method returns the value of the property.
* The type of the value depends on the type of the property.
* @return the value of the property
*/
public Object getValue() {
return value;
}
/**
* @return the names of properties that are stored withing this property
* (assuming this property is of IDP_GROUP type)
*/
@SuppressWarnings("unchecked")
public List<String> getSubPropertiesNames() {
List<String> result = null;
if (type == IDP_GROUP) {
List<Properties> properties = (List<Properties>) value;
if (properties != null && properties.size() > 0) {
result = new ArrayList<String>(properties.size());
for (Properties property : properties) {
result.add(property.getName());
}
}
}
return result;
}
/**
* This method returns the same as getValue if the current property is of
* other type than IDP_GROUP and its name matches 'propertyName' param. If
* this property is a group property the method tries to find subproperty
* value of the given name. The first found value is returnes os <b>use this
* method wisely</b>. If no property of a given name is foung - <b>null</b>
* is returned.
*
* @param propertyName
* the name of the property
* @return found property value or <b>null</b>
*/
@SuppressWarnings("unchecked")
public Object findValue(String propertyName) {
if (name.equals(propertyName)) {
return value;
} else {
if (type == IDP_GROUP) {
List<Properties> props = (List<Properties>) value;
for (Properties p : props) {
Object v = p.findValue(propertyName);
if (v != null) {
return v;
}
}
}
}
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
this.append(sb, new StringBuilder());
return sb.toString();
}
/**
* This method appends the data of the property to the given string buffer.
* @param sb
* string buffer
* @param indent
* indent buffer
*/
@SuppressWarnings("unchecked")
private void append(StringBuilder sb, StringBuilder indent) {
sb.append(indent).append("name: ").append(name).append("\n\r");
sb.append(indent).append("type: ").append(type).append("\n\r");
sb.append(indent).append("subType: ").append(subType).append("\n\r");
sb.append(indent).append("description: ").append(description).append("\n\r");
indent.append('\t');
sb.append(indent).append("value: ");
if (value instanceof Properties) {
((Properties) value).append(sb, indent);
} else if (value instanceof List) {
for (Object v : (List<Object>) value) {
if (v instanceof Properties) {
sb.append(indent).append("{\n\r");
indent.append('\t');
((Properties) v).append(sb, indent);
indent.deleteCharAt(indent.length() - 1);
sb.append(indent).append("}\n\r");
} else {
sb.append(v);
}
}
} else {
sb.append(value);
}
sb.append("\n\r");
indent.deleteCharAt(indent.length() - 1);
}
/**
* This method should be called after the properties loading.
* It loads the properties from the _RNA_UI property and removes this property from the
* result list.
*/
@SuppressWarnings("unchecked")
protected void completeLoading() {
if (type == IDP_GROUP) {
List<Properties> groupProperties = (List<Properties>) value;
Properties rnaUI = null;
for (Properties properties : groupProperties) {
if (properties.name.equals(RNA_PROPERTY_NAME) && properties.type == IDP_GROUP) {
rnaUI = properties;
break;
}
}
if (rnaUI != null) {
// removing the RNA from the result list
groupProperties.remove(rnaUI);
// loading the descriptions
Map<String, String> descriptions = new HashMap<String, String>(groupProperties.size());
List<Properties> propertiesRNA = (List<Properties>) rnaUI.value;
for (Properties properties : propertiesRNA) {
String name = properties.name;
String description = null;
List<Properties> rnaData = (List<Properties>) properties.value;
for (Properties rna : rnaData) {
if ("description".equalsIgnoreCase(rna.name)) {
description = (String) rna.value;
break;
}
}
descriptions.put(name, description);
}
// applying the descriptions
for (Properties properties : groupProperties) {
properties.description = descriptions.get(properties.name);
}
}
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (description == null ? 0 : description.hashCode());
result = prime * result + (name == null ? 0 : name.hashCode());
result = prime * result + subType;
result = prime * result + type;
result = prime * result + (value == null ? 0 : value.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
Properties other = (Properties) obj;
if (description == null) {
if (other.description != null) {
return false;
}
} else if (!description.equals(other.description)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
if (subType != other.subType) {
return false;
}
if (type != other.type) {
return false;
}
if (value == null) {
if (other.value != null) {
return false;
}
} else if (!value.equals(other.value)) {
return false;
}
return true;
}
}

@ -0,0 +1,192 @@
package com.jme3.scene.plugins.blender.particles;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh.Type;
import com.jme3.effect.influencers.EmptyParticleInfluencer;
import com.jme3.effect.influencers.NewtonianParticleInfluencer;
import com.jme3.effect.influencers.ParticleInfluencer;
import com.jme3.effect.shapes.EmitterMeshConvexHullShape;
import com.jme3.effect.shapes.EmitterMeshFaceShape;
import com.jme3.effect.shapes.EmitterMeshVertexShape;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import java.util.logging.Logger;
public class ParticlesHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(ParticlesHelper.class.getName());
// part->type
public static final int PART_EMITTER = 0;
public static final int PART_REACTOR = 1;
public static final int PART_HAIR = 2;
public static final int PART_FLUID = 3;
// part->flag
public static final int PART_REACT_STA_END = 1;
public static final int PART_REACT_MULTIPLE = 2;
public static final int PART_LOOP = 4;
// public static final int PART_LOOP_INSTANT =8;
public static final int PART_HAIR_GEOMETRY = 16;
public static final int PART_UNBORN = 32; // show unborn particles
public static final int PART_DIED = 64; // show died particles
public static final int PART_TRAND = 128;
public static final int PART_EDISTR = 256; // particle/face from face areas
public static final int PART_STICKY = 512; // collided particles can stick to collider
public static final int PART_DIE_ON_COL = 1 << 12;
public static final int PART_SIZE_DEFL = 1 << 13; // swept sphere deflections
public static final int PART_ROT_DYN = 1 << 14; // dynamic rotation
public static final int PART_SIZEMASS = 1 << 16;
public static final int PART_ABS_LENGTH = 1 << 15;
public static final int PART_ABS_TIME = 1 << 17;
public static final int PART_GLOB_TIME = 1 << 18;
public static final int PART_BOIDS_2D = 1 << 19;
public static final int PART_BRANCHING = 1 << 20;
public static final int PART_ANIM_BRANCHING = 1 << 21;
public static final int PART_SELF_EFFECT = 1 << 22;
public static final int PART_SYMM_BRANCHING = 1 << 24;
public static final int PART_HAIR_BSPLINE = 1024;
public static final int PART_GRID_INVERT = 1 << 26;
public static final int PART_CHILD_EFFECT = 1 << 27;
public static final int PART_CHILD_SEAMS = 1 << 28;
public static final int PART_CHILD_RENDER = 1 << 29;
public static final int PART_CHILD_GUIDE = 1 << 30;
// part->from
public static final int PART_FROM_VERT = 0;
public static final int PART_FROM_FACE = 1;
public static final int PART_FROM_VOLUME = 2;
public static final int PART_FROM_PARTICLE = 3;
public static final int PART_FROM_CHILD = 4;
// part->phystype
public static final int PART_PHYS_NO = 0;
public static final int PART_PHYS_NEWTON = 1;
public static final int PART_PHYS_KEYED = 2;
public static final int PART_PHYS_BOIDS = 3;
// part->draw_as
public static final int PART_DRAW_NOT = 0;
public static final int PART_DRAW_DOT = 1;
public static final int PART_DRAW_CIRC = 2;
public static final int PART_DRAW_CROSS = 3;
public static final int PART_DRAW_AXIS = 4;
public static final int PART_DRAW_LINE = 5;
public static final int PART_DRAW_PATH = 6;
public static final int PART_DRAW_OB = 7;
public static final int PART_DRAW_GR = 8;
public static final int PART_DRAW_BB = 9;
/**
* This constructor parses the given blender version and stores the result. Some functionalities may differ in
* different blender versions.
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public ParticlesHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
@SuppressWarnings("unchecked")
public ParticleEmitter toParticleEmitter(Structure particleSystem) throws BlenderFileException {
ParticleEmitter result = null;
Pointer pParticleSettings = (Pointer) particleSystem.getFieldValue("part");
if (pParticleSettings.isNotNull()) {
Structure particleSettings = pParticleSettings.fetchData().get(0);
int totPart = ((Number) particleSettings.getFieldValue("totpart")).intValue();
// draw type will be stored temporarily in the name (it is used during modifier applying operation)
int drawAs = ((Number) particleSettings.getFieldValue("draw_as")).intValue();
char nameSuffix;// P - point, L - line, N - None, B - Bilboard
switch (drawAs) {
case PART_DRAW_NOT:
nameSuffix = 'N';
totPart = 0;// no need to generate particles in this case
break;
case PART_DRAW_BB:
nameSuffix = 'B';
break;
case PART_DRAW_OB:
case PART_DRAW_GR:
nameSuffix = 'P';
LOGGER.warning("Neither object nor group particles supported yet! Using point representation instead!");// TODO: support groups and aobjects
break;
case PART_DRAW_LINE:
nameSuffix = 'L';
LOGGER.warning("Lines not yet supported! Using point representation instead!");// TODO: support lines
default:// all others are rendered as points in blender
nameSuffix = 'P';
}
result = new ParticleEmitter(particleSettings.getName() + nameSuffix, Type.Triangle, totPart);
if (nameSuffix == 'N') {
return result;// no need to set anything else
}
// setting the emitters shape (the shapes meshes will be set later during modifier applying operation)
int from = ((Number) particleSettings.getFieldValue("from")).intValue();
switch (from) {
case PART_FROM_VERT:
result.setShape(new EmitterMeshVertexShape());
break;
case PART_FROM_FACE:
result.setShape(new EmitterMeshFaceShape());
break;
case PART_FROM_VOLUME:
result.setShape(new EmitterMeshConvexHullShape());
break;
default:
LOGGER.warning("Default shape used! Unknown emitter shape value ('from' parameter: " + from + ')');
}
// reading acceleration
DynamicArray<Number> acc = (DynamicArray<Number>) particleSettings.getFieldValue("acc");
result.setGravity(-acc.get(0).floatValue(), -acc.get(1).floatValue(), -acc.get(2).floatValue());
// setting the colors
result.setEndColor(new ColorRGBA(1f, 1f, 1f, 1f));
result.setStartColor(new ColorRGBA(1f, 1f, 1f, 1f));
// reading size
float sizeFactor = nameSuffix == 'B' ? 1.0f : 0.3f;
float size = ((Number) particleSettings.getFieldValue("size")).floatValue() * sizeFactor;
result.setStartSize(size);
result.setEndSize(size);
// reading lifetime
int fps = blenderContext.getBlenderKey().getFps();
float lifetime = ((Number) particleSettings.getFieldValue("lifetime")).floatValue() / fps;
float randlife = ((Number) particleSettings.getFieldValue("randlife")).floatValue() / fps;
result.setLowLife(lifetime * (1.0f - randlife));
result.setHighLife(lifetime);
// preparing influencer
ParticleInfluencer influencer;
int phystype = ((Number) particleSettings.getFieldValue("phystype")).intValue();
switch (phystype) {
case PART_PHYS_NEWTON:
influencer = new NewtonianParticleInfluencer();
((NewtonianParticleInfluencer) influencer).setNormalVelocity(((Number) particleSettings.getFieldValue("normfac")).floatValue());
((NewtonianParticleInfluencer) influencer).setVelocityVariation(((Number) particleSettings.getFieldValue("randfac")).floatValue());
((NewtonianParticleInfluencer) influencer).setSurfaceTangentFactor(((Number) particleSettings.getFieldValue("tanfac")).floatValue());
((NewtonianParticleInfluencer) influencer).setSurfaceTangentRotation(((Number) particleSettings.getFieldValue("tanphase")).floatValue());
break;
case PART_PHYS_BOIDS:
case PART_PHYS_KEYED:// TODO: support other influencers
LOGGER.warning("Boids and Keyed particles physic not yet supported! Empty influencer used!");
case PART_PHYS_NO:
default:
influencer = new EmptyParticleInfluencer();
}
result.setParticleInfluencer(influencer);
}
return result;
}
}

@ -0,0 +1,401 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A class constaining the colorband data.
*
* @author Marcin Roguski (Kaelthas)
*/
public class ColorBand {
private static final Logger LOGGER = Logger.getLogger(ColorBand.class.getName());
// interpolation types
public static final int IPO_LINEAR = 0;
public static final int IPO_EASE = 1;
public static final int IPO_BSPLINE = 2;
public static final int IPO_CARDINAL = 3;
public static final int IPO_CONSTANT = 4;
private int cursorsAmount, ipoType;
/** The default amount of possible cursor positions. */
private int resultSize = 1001;
private ColorBandData[] data;
/**
* A constructor used to instantiate color band by hand instead of reading it from the blend file.
* @param ipoType
* the interpolation type
* @param colors
* the colorband colors
* @param positions
* the positions for colors' cursors
* @param resultSize
* the size of the result table
*/
public ColorBand(int ipoType, List<ColorRGBA> colors, List<Integer> positions, int resultSize) {
if (colors == null || colors.size() < 1) {
throw new IllegalArgumentException("The amount of colorband's colors must be at least 1.");
}
if (ipoType < IPO_LINEAR || ipoType > IPO_CONSTANT) {
throw new IllegalArgumentException("Unknown colorband interpolation type: " + ipoType);
}
if (positions == null || positions.size() != colors.size()) {
throw new IllegalArgumentException("The size of positions and colors list should be equal!");
}
for (Integer position : positions) {
if (position.intValue() < 0 || position.intValue() >= resultSize) {
throw new IllegalArgumentException("Invalid position value: " + position + "! Should be from range: [0, " + resultSize + "]!");
}
}
cursorsAmount = colors.size();
this.ipoType = ipoType;
this.resultSize = resultSize;
data = new ColorBandData[cursorsAmount];
for (int i = 0; i < cursorsAmount; ++i) {
data[i] = new ColorBandData(colors.get(i), positions.get(i));
}
}
/**
* Constructor. Loads the data from the given structure.
* @param tex
* @param blenderContext
*/
@SuppressWarnings("unchecked")
public ColorBand(Structure tex, BlenderContext blenderContext) {
int flag = ((Number) tex.getFieldValue("flag")).intValue();
if ((flag & GeneratedTexture.TEX_COLORBAND) != 0) {
Pointer pColorband = (Pointer) tex.getFieldValue("coba");
try {
Structure colorbandStructure = pColorband.fetchData().get(0);
cursorsAmount = ((Number) colorbandStructure.getFieldValue("tot")).intValue();
ipoType = ((Number) colorbandStructure.getFieldValue("ipotype")).intValue();
data = new ColorBandData[cursorsAmount];
DynamicArray<Structure> data = (DynamicArray<Structure>) colorbandStructure.getFieldValue("data");
for (int i = 0; i < cursorsAmount; ++i) {
this.data[i] = new ColorBandData(data.get(i));
}
} catch (BlenderFileException e) {
LOGGER.log(Level.WARNING, "Cannot fetch the colorband structure. The reason: {0}", e.getLocalizedMessage());
}
}
}
/**
* This method determines if the colorband has any transparencies or is not
* transparent at all.
*
* @return <b>true</b> if the colorband has transparencies and <b>false</b>
* otherwise
*/
public boolean hasTransparencies() {
if (data != null) {
for (ColorBandData colorBandData : data) {
if (colorBandData.a < 1.0f) {
return true;
}
}
}
return false;
}
/**
* This method computes the values of the colorband.
*
* @return an array of 1001 elements and each element is float[4] object
* containing rgba values
*/
public float[][] computeValues() {
float[][] result = null;
if (data != null) {
result = new float[resultSize][4];// resultSize - amount of possible cursor positions; 4 = [r, g, b, a]
if (data.length == 1) {// special case; use only one color for all types of colorband interpolation
for (int i = 0; i < result.length; ++i) {
result[i][0] = data[0].r;
result[i][1] = data[0].g;
result[i][2] = data[0].b;
result[i][3] = data[0].a;
}
} else {
int currentCursor = 0;
ColorBandData currentData = data[0];
ColorBandData nextData = data[0];
switch (ipoType) {
case ColorBand.IPO_LINEAR:
float rDiff = 0,
gDiff = 0,
bDiff = 0,
aDiff = 0,
posDiff;
for (int i = 0; i < result.length; ++i) {
posDiff = i - currentData.pos;
result[i][0] = currentData.r + rDiff * posDiff;
result[i][1] = currentData.g + gDiff * posDiff;
result[i][2] = currentData.b + bDiff * posDiff;
result[i][3] = currentData.a + aDiff * posDiff;
if (nextData.pos == i) {
currentData = data[currentCursor++];
if (currentCursor < data.length) {
nextData = data[currentCursor];
// calculate differences
int d = nextData.pos - currentData.pos;
rDiff = (nextData.r - currentData.r) / d;
gDiff = (nextData.g - currentData.g) / d;
bDiff = (nextData.b - currentData.b) / d;
aDiff = (nextData.a - currentData.a) / d;
} else {
rDiff = gDiff = bDiff = aDiff = 0;
}
}
}
break;
case ColorBand.IPO_BSPLINE:
case ColorBand.IPO_CARDINAL:
Map<Integer, ColorBandData> cbDataMap = new TreeMap<Integer, ColorBandData>();
for (int i = 0; i < data.length; ++i) {
cbDataMap.put(Integer.valueOf(i), data[i]);
}
if (data[0].pos == 0) {
cbDataMap.put(Integer.valueOf(-1), data[0]);
} else {
ColorBandData cbData = new ColorBandData(data[0]);
cbData.pos = 0;
cbDataMap.put(Integer.valueOf(-1), cbData);
cbDataMap.put(Integer.valueOf(-2), cbData);
}
if (data[data.length - 1].pos == 1000) {
cbDataMap.put(Integer.valueOf(data.length), data[data.length - 1]);
} else {
ColorBandData cbData = new ColorBandData(data[data.length - 1]);
cbData.pos = 1000;
cbDataMap.put(Integer.valueOf(data.length), cbData);
cbDataMap.put(Integer.valueOf(data.length + 1), cbData);
}
float[] ipoFactors = new float[4];
float f;
ColorBandData data0 = this.getColorbandData(currentCursor - 2, cbDataMap);
ColorBandData data1 = this.getColorbandData(currentCursor - 1, cbDataMap);
ColorBandData data2 = this.getColorbandData(currentCursor, cbDataMap);
ColorBandData data3 = this.getColorbandData(currentCursor + 1, cbDataMap);
for (int i = 0; i < result.length; ++i) {
if (data2.pos != data1.pos) {
f = (i - data2.pos) / (float) (data1.pos - data2.pos);
f = FastMath.clamp(f, 0.0f, 1.0f);
} else {
f = 0.0f;
}
this.getIpoData(f, ipoFactors);
result[i][0] = ipoFactors[3] * data0.r + ipoFactors[2] * data1.r + ipoFactors[1] * data2.r + ipoFactors[0] * data3.r;
result[i][1] = ipoFactors[3] * data0.g + ipoFactors[2] * data1.g + ipoFactors[1] * data2.g + ipoFactors[0] * data3.g;
result[i][2] = ipoFactors[3] * data0.b + ipoFactors[2] * data1.b + ipoFactors[1] * data2.b + ipoFactors[0] * data3.b;
result[i][3] = ipoFactors[3] * data0.a + ipoFactors[2] * data1.a + ipoFactors[1] * data2.a + ipoFactors[0] * data3.a;
result[i][0] = FastMath.clamp(result[i][0], 0.0f, 1.0f);
result[i][1] = FastMath.clamp(result[i][1], 0.0f, 1.0f);
result[i][2] = FastMath.clamp(result[i][2], 0.0f, 1.0f);
result[i][3] = FastMath.clamp(result[i][3], 0.0f, 1.0f);
if (nextData.pos == i) {
++currentCursor;
data0 = cbDataMap.get(currentCursor - 2);
data1 = cbDataMap.get(currentCursor - 1);
data2 = cbDataMap.get(currentCursor);
data3 = cbDataMap.get(currentCursor + 1);
}
}
break;
case ColorBand.IPO_EASE:
float d,
a,
b,
d2;
for (int i = 0; i < result.length; ++i) {
if (nextData.pos != currentData.pos) {
d = (i - currentData.pos) / (float) (nextData.pos - currentData.pos);
d2 = d * d;
a = 3.0f * d2 - 2.0f * d * d2;
b = 1.0f - a;
} else {
d = a = 0.0f;
b = 1.0f;
}
result[i][0] = b * currentData.r + a * nextData.r;
result[i][1] = b * currentData.g + a * nextData.g;
result[i][2] = b * currentData.b + a * nextData.b;
result[i][3] = b * currentData.a + a * nextData.a;
if (nextData.pos == i) {
currentData = data[currentCursor++];
if (currentCursor < data.length) {
nextData = data[currentCursor];
}
}
}
break;
case ColorBand.IPO_CONSTANT:
for (int i = 0; i < result.length; ++i) {
result[i][0] = currentData.r;
result[i][1] = currentData.g;
result[i][2] = currentData.b;
result[i][3] = currentData.a;
if (nextData.pos == i) {
currentData = data[currentCursor++];
if (currentCursor < data.length) {
nextData = data[currentCursor];
}
}
}
break;
default:
throw new IllegalStateException("Unknown interpolation type: " + ipoType);
}
}
}
return result;
}
private ColorBandData getColorbandData(int index, Map<Integer, ColorBandData> cbDataMap) {
ColorBandData result = cbDataMap.get(index);
if (result == null) {
result = new ColorBandData();
}
return result;
}
/**
* This method returns the data for either B-spline of Cardinal
* interpolation.
*
* @param d
* distance factor for the current intensity
* @param ipoFactors
* table to store the results (size of the table must be at least
* 4)
*/
private void getIpoData(float d, float[] ipoFactors) {
float d2 = d * d;
float d3 = d2 * d;
if (ipoType == ColorBand.IPO_BSPLINE) {
ipoFactors[0] = -0.71f * d3 + 1.42f * d2 - 0.71f * d;
ipoFactors[1] = 1.29f * d3 - 2.29f * d2 + 1.0f;
ipoFactors[2] = -1.29f * d3 + 1.58f * d2 + 0.71f * d;
ipoFactors[3] = 0.71f * d3 - 0.71f * d2;
} else if (ipoType == ColorBand.IPO_CARDINAL) {
ipoFactors[0] = -0.16666666f * d3 + 0.5f * d2 - 0.5f * d + 0.16666666f;
ipoFactors[1] = 0.5f * d3 - d2 + 0.6666666f;
ipoFactors[2] = -0.5f * d3 + 0.5f * d2 + 0.5f * d + 0.16666666f;
ipoFactors[3] = 0.16666666f * d3;
} else {
throw new IllegalStateException("Cannot get interpolation data for other colorband types than B-spline and Cardinal!");
}
}
/**
* Class to store the single colorband cursor data.
*
* @author Marcin Roguski (Kaelthas)
*/
private static class ColorBandData {
public final float r, g, b, a;
public int pos;
public ColorBandData() {
r = g = b = 0;
a = 1;
}
/**
* Constructor that stores the color and position of the cursor.
* @param color
* the cursor's color
* @param pos
* the cursor's position
*/
public ColorBandData(ColorRGBA color, int pos) {
r = color.r;
g = color.g;
b = color.b;
a = color.a;
this.pos = pos;
}
/**
* Copy constructor.
*/
private ColorBandData(ColorBandData data) {
r = data.r;
g = data.g;
b = data.b;
a = data.a;
pos = data.pos;
}
/**
* Constructor. Loads the data from the given structure.
*
* @param cbdataStructure
* the structure containing the CBData object
*/
public ColorBandData(Structure cbdataStructure) {
r = ((Number) cbdataStructure.getFieldValue("r")).floatValue();
g = ((Number) cbdataStructure.getFieldValue("g")).floatValue();
b = ((Number) cbdataStructure.getFieldValue("b")).floatValue();
a = ((Number) cbdataStructure.getFieldValue("a")).floatValue();
pos = (int) (((Number) cbdataStructure.getFieldValue("pos")).floatValue() * 1000.0f);
}
@Override
public String toString() {
return "P: " + pos + " [" + r + ", " + g + ", " + b + ", " + a + "]";
}
}
}

@ -0,0 +1,543 @@
package com.jme3.scene.plugins.blender.textures;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3tools.converters.ImageToAwt;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
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.textures.TriangulatedTexture.TriangleTextureElement;
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType;
import com.jme3.scene.plugins.blender.textures.UVProjectionGenerator.UVProjectionType;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlenderFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.MagFilter;
import com.jme3.texture.Texture.MinFilter;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.texture.Texture2D;
import com.jme3.texture.TextureCubeMap;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
/**
* This class represents a texture that is defined for the material. It can be
* made of several textures (both 2D and 3D) that are merged together and
* returned as a single texture.
*
* @author Marcin Roguski (Kaelthas)
*/
public class CombinedTexture {
private static final Logger LOGGER = Logger.getLogger(CombinedTexture.class.getName());
/** The mapping type of the texture. Defined bu MaterialContext.MTEX_COL, MTEX_NOR etc. */
private final int mappingType;
/**
* If set to true then if a texture without alpha is added then all textures below are discarded because
* the new one will cover them anyway. If set to false then all textures are stored.
*/
private boolean discardCoveredTextures;
/** The data for each of the textures. */
private List<TextureData> textureDatas = new ArrayList<TextureData>();
/** The result texture. */
private Texture resultTexture;
/** The UV values for the result texture. */
private List<Vector2f> resultUVS;
/**
* Constructor. Stores the texture mapping type (ie. color map, normal map).
*
* @param mappingType
* texture mapping type
* @param discardCoveredTextures
* if set to true then if a texture without alpha is added then all textures below are discarded because
* the new one will cover them anyway, if set to false then all textures are stored
*/
public CombinedTexture(int mappingType, boolean discardCoveredTextures) {
this.mappingType = mappingType;
this.discardCoveredTextures = discardCoveredTextures;
}
/**
* This method adds a texture data to the resulting texture.
*
* @param texture
* the source texture
* @param textureBlender
* the texture blender (to mix the texture with its material
* color)
* @param uvCoordinatesType
* the type of UV coordinates
* @param projectionType
* the type of UV coordinates projection (for flat textures)
* @param textureStructure
* the texture sructure
* @param uvCoordinatesName
* the name of the used user's UV coordinates for this texture
* @param blenderContext
* the blender context
*/
public void add(Texture texture, TextureBlender textureBlender, int uvCoordinatesType, int projectionType, Structure textureStructure, String uvCoordinatesName, BlenderContext blenderContext) {
if (!(texture instanceof GeneratedTexture) && !(texture instanceof Texture2D)) {
throw new IllegalArgumentException("Unsupported texture type: " + (texture == null ? "null" : texture.getClass()));
}
if (!(texture instanceof GeneratedTexture) || blenderContext.getBlenderKey().isLoadGeneratedTextures()) {
if (UVCoordinatesGenerator.isTextureCoordinateTypeSupported(UVCoordinatesType.valueOf(uvCoordinatesType))) {
TextureData textureData = new TextureData();
textureData.texture = texture;
textureData.textureBlender = textureBlender;
textureData.uvCoordinatesType = UVCoordinatesType.valueOf(uvCoordinatesType);
textureData.projectionType = UVProjectionType.valueOf(projectionType);
textureData.textureStructure = textureStructure;
textureData.uvCoordinatesName = uvCoordinatesName;
if (discardCoveredTextures && textureDatas.size() > 0 && this.isWithoutAlpha(textureData, blenderContext)) {
textureDatas.clear();// clear previous textures, they will be covered anyway
}
textureDatas.add(textureData);
} else {
LOGGER.warning("The texture coordinates type is not supported: " + UVCoordinatesType.valueOf(uvCoordinatesType) + ". The texture '" + textureStructure.getName() + "'.");
}
}
}
/**
* This method flattens the texture and creates a single result of Texture2D
* type.
*
* @param geometry
* the geometry the texture is created for
* @param geometriesOMA
* the old memory address of the geometries list that the given
* geometry belongs to (needed for bounding box creation)
* @param userDefinedUVCoordinates
* the UV's defined by user (null or zero length table if none
* were defined)
* @param blenderContext
* the blender context
* @return the name of the user UV coordinates used (null if the UV's were
* generated)
*/
public String flatten(Geometry geometry, Long geometriesOMA, Map<String, List<Vector2f>> userDefinedUVCoordinates, BlenderContext blenderContext) {
Mesh mesh = geometry.getMesh();
Texture previousTexture = null;
UVCoordinatesType masterUVCoordinatesType = null;
String masterUserUVSetName = null;
for (TextureData textureData : textureDatas) {
// decompress compressed textures (all will be merged into one texture anyway)
if (textureDatas.size() > 1 && textureData.texture.getImage().getFormat().isCompressed()) {
textureData.texture.setImage(ImageUtils.decompress(textureData.texture.getImage()));
textureData.textureBlender = TextureBlenderFactory.alterTextureType(textureData.texture.getImage().getFormat(), textureData.textureBlender);
}
if (previousTexture == null) {// the first texture will lead the others to its shape
if (textureData.texture instanceof GeneratedTexture) {
resultTexture = ((GeneratedTexture) textureData.texture).triangulate(mesh, geometriesOMA, textureData.uvCoordinatesType, blenderContext);
} else if (textureData.texture instanceof Texture2D) {
resultTexture = textureData.texture;
if (textureData.uvCoordinatesType == UVCoordinatesType.TEXCO_UV && userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) {
if (textureData.uvCoordinatesName == null) {
resultUVS = userDefinedUVCoordinates.values().iterator().next();// get the first UV available
} else {
resultUVS = userDefinedUVCoordinates.get(textureData.uvCoordinatesName);
}
if(resultUVS == null && LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("The texture " + textureData.texture.getName() + " has assigned non existing UV coordinates group: " + textureData.uvCoordinatesName + ".");
}
masterUserUVSetName = textureData.uvCoordinatesName;
} else {
TemporalMesh temporalMesh = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH);
resultUVS = UVCoordinatesGenerator.generateUVCoordinatesFor2DTexture(mesh, textureData.uvCoordinatesType, textureData.projectionType, temporalMesh);
}
}
this.blend(resultTexture, textureData.textureBlender, blenderContext);
previousTexture = resultTexture;
masterUVCoordinatesType = textureData.uvCoordinatesType;
} else {
if (textureData.texture instanceof GeneratedTexture) {
if (!(resultTexture instanceof TriangulatedTexture)) {
resultTexture = new TriangulatedTexture((Texture2D) resultTexture, resultUVS, blenderContext);
resultUVS = null;
previousTexture = resultTexture;
}
TriangulatedTexture triangulatedTexture = ((GeneratedTexture) textureData.texture).triangulate(mesh, geometriesOMA, textureData.uvCoordinatesType, blenderContext);
triangulatedTexture.castToUVS((TriangulatedTexture) resultTexture, blenderContext);
triangulatedTexture.blend(textureData.textureBlender, (TriangulatedTexture) resultTexture, blenderContext);
resultTexture = previousTexture = triangulatedTexture;
} else if (textureData.texture instanceof Texture2D) {
if (this.isUVTypesMatch(masterUVCoordinatesType, masterUserUVSetName, textureData.uvCoordinatesType, textureData.uvCoordinatesName) && resultTexture instanceof Texture2D) {
this.scale((Texture2D) textureData.texture, resultTexture.getImage().getWidth(), resultTexture.getImage().getHeight());
ImageUtils.merge(resultTexture.getImage(), textureData.texture.getImage());
previousTexture = resultTexture;
} else {
if (!(resultTexture instanceof TriangulatedTexture)) {
resultTexture = new TriangulatedTexture((Texture2D) resultTexture, resultUVS, blenderContext);
resultUVS = null;
}
// first triangulate the current texture
List<Vector2f> textureUVS = null;
if (textureData.uvCoordinatesType == UVCoordinatesType.TEXCO_UV && userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) {
if (textureData.uvCoordinatesName == null) {
textureUVS = userDefinedUVCoordinates.values().iterator().next();// get the first UV available
} else {
textureUVS = userDefinedUVCoordinates.get(textureData.uvCoordinatesName);
}
} else {
TemporalMesh geometries = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH);
textureUVS = UVCoordinatesGenerator.generateUVCoordinatesFor2DTexture(mesh, textureData.uvCoordinatesType, textureData.projectionType, geometries);
}
TriangulatedTexture triangulatedTexture = new TriangulatedTexture((Texture2D) textureData.texture, textureUVS, blenderContext);
// then move the texture to different UV's
triangulatedTexture.castToUVS((TriangulatedTexture) resultTexture, blenderContext);
// merge triangulated textures
for (int i = 0; i < ((TriangulatedTexture) resultTexture).getFaceTextureCount(); ++i) {
ImageUtils.merge(((TriangulatedTexture) resultTexture).getFaceTextureElement(i).image, triangulatedTexture.getFaceTextureElement(i).image);
}
}
}
}
}
if (resultTexture instanceof TriangulatedTexture) {
if (mappingType == MaterialContext.MTEX_NOR) {
for (int i = 0; i < ((TriangulatedTexture) resultTexture).getFaceTextureCount(); ++i) {
TriangleTextureElement triangleTextureElement = ((TriangulatedTexture) resultTexture).getFaceTextureElement(i);
triangleTextureElement.image = ImageUtils.convertToNormalMapTexture(triangleTextureElement.image, 1);// TODO: get proper strength factor
}
}
resultUVS = ((TriangulatedTexture) resultTexture).getResultUVS();
resultTexture = ((TriangulatedTexture) resultTexture).getResultTexture();
masterUserUVSetName = null;
}
// setting additional data
resultTexture.setWrap(WrapMode.Repeat);
// the filters are required if generated textures are used because
// otherwise ugly lines appear between the mesh faces
resultTexture.setMagFilter(MagFilter.Nearest);
resultTexture.setMinFilter(MinFilter.NearestNoMipMaps);
return masterUserUVSetName;
}
/**
* Generates a texture that will be used by the sky spatial.
* The result texture has 6 layers. Every image in each layer has equal size and its shape is a square.
* The size of each image is the maximum size (width or height) of the textures given.
* The default sky generated texture size is used (this value is set in the BlenderKey) if no picture textures
* are present or their sizes is lower than the generated texture size.
* The textures of lower sizes are properly scaled.
* All the textures are mixed into one and put as layers in the result texture.
*
* @param horizontalColor
* the horizon color
* @param zenithColor
* the zenith color
* @param blenderContext
* the blender context
* @return texture for the sky
*/
public TextureCubeMap generateSkyTexture(ColorRGBA horizontalColor, ColorRGBA zenithColor, BlenderContext blenderContext) {
LOGGER.log(Level.FINE, "Preparing sky texture from {0} applied textures.", textureDatas.size());
LOGGER.fine("Computing the texture size.");
int size = -1;
for (TextureData textureData : textureDatas) {
if (textureData.texture instanceof Texture2D) {
size = Math.max(textureData.texture.getImage().getWidth(), size);
size = Math.max(textureData.texture.getImage().getHeight(), size);
}
}
if (size < 0) {
size = blenderContext.getBlenderKey().getSkyGeneratedTextureSize();
}
LOGGER.log(Level.FINE, "The sky texture size will be: {0}x{0}.", size);
TextureCubeMap result = null;
for (TextureData textureData : textureDatas) {
TextureCubeMap texture = null;
if (textureData.texture instanceof GeneratedTexture) {
texture = ((GeneratedTexture) textureData.texture).generateSkyTexture(size, horizontalColor, zenithColor, blenderContext);
} else {
// first create a grayscale version of the image
Image image = textureData.texture.getImage();
if (image.getWidth() != image.getHeight() || image.getWidth() != size) {
image = ImageUtils.resizeTo(image, size, size);
}
Image grayscaleImage = ImageUtils.convertToGrayscaleTexture(image);
// add the sky colors to the image
PixelInputOutput sourcePixelIO = PixelIOFactory.getPixelIO(grayscaleImage.getFormat());
PixelInputOutput targetPixelIO = PixelIOFactory.getPixelIO(image.getFormat());
TexturePixel texturePixel = new TexturePixel();
for (int x = 0; x < image.getWidth(); ++x) {
for (int y = 0; y < image.getHeight(); ++y) {
sourcePixelIO.read(grayscaleImage, 0, texturePixel, x, y);
texturePixel.intensity = texturePixel.red;// no matter which factor we use here, in grayscale they are all equal
ImageUtils.color(texturePixel, horizontalColor, zenithColor);
targetPixelIO.write(image, 0, texturePixel, x, y);
}
}
// create the cubemap texture from the coloured image
ByteBuffer sourceData = image.getData(0);
ArrayList<ByteBuffer> data = new ArrayList<ByteBuffer>(6);
for (int i = 0; i < 6; ++i) {
data.add(BufferUtils.clone(sourceData));
}
texture = new TextureCubeMap(new Image(image.getFormat(), image.getWidth(), image.getHeight(), 6, data, ColorSpace.Linear));
}
if (result == null) {
result = texture;
} else {
ImageUtils.mix(result.getImage(), texture.getImage());
}
}
return result;
}
/**
* The method checks if the texture UV coordinates match.
* It the types are equal and different then UVCoordinatesType.TEXCO_UV then we consider them a match.
* If they are both UVCoordinatesType.TEXCO_UV then they match only when their UV sets names are equal.
* In other cases they are considered NOT a match.
* @param type1
* the UV coord type
* @param uvSetName1
* the user's UV coords set name (considered only for UVCoordinatesType.TEXCO_UV)
* @param type2
* the UV coord type
* @param uvSetName2
* the user's UV coords set name (considered only for UVCoordinatesType.TEXCO_UV)
* @return <b>true</b> if the types match and <b>false</b> otherwise
*/
private boolean isUVTypesMatch(UVCoordinatesType type1, String uvSetName1, UVCoordinatesType type2, String uvSetName2) {
if (type1 == type2) {
if (type1 == UVCoordinatesType.TEXCO_UV) {
if (uvSetName1 != null && uvSetName2 != null && uvSetName1.equals(uvSetName2)) {
return true;
}
} else {
return true;
}
}
return false;
}
/**
* This method blends the texture.
*
* @param texture
* the texture to be blended
* @param textureBlender
* blending definition for the texture
* @param blenderContext
* the blender context
*/
private void blend(Texture texture, TextureBlender textureBlender, BlenderContext blenderContext) {
if (texture instanceof TriangulatedTexture) {
((TriangulatedTexture) texture).blend(textureBlender, null, blenderContext);
} else if (texture instanceof Texture2D) {
Image blendedImage = textureBlender.blend(texture.getImage(), null, blenderContext);
texture.setImage(blendedImage);
} else {
throw new IllegalArgumentException("Invalid type for texture to blend!");
}
}
/**
* @return the result texture
*/
public Texture getResultTexture() {
return resultTexture;
}
/**
* @return the result UV coordinates
*/
public List<Vector2f> getResultUVS() {
return resultUVS;
}
/**
* @return the amount of added textures
*/
public int getTexturesCount() {
return textureDatas.size();
}
/**
* @return the texture's mapping type
*/
public int getMappingType() {
return mappingType;
}
/**
* @return <b>true</b> if the texture has at least one generated texture component and <b>false</b> otherwise
*/
public boolean hasGeneratedTextures() {
if (textureDatas != null) {
for (TextureData textureData : textureDatas) {
if (textureData.texture instanceof GeneratedTexture) {
return true;
}
}
}
return false;
}
/**
* This method determines if the given texture has no alpha channel.
*
* @param texture
* the texture to check for alpha channel
* @return <b>true</b> if the texture has no alpha channel and <b>false</b>
* otherwise
*/
private boolean isWithoutAlpha(TextureData textureData, BlenderContext blenderContext) {
ColorBand colorBand = new ColorBand(textureData.textureStructure, blenderContext);
if (!colorBand.hasTransparencies()) {
int type = ((Number) textureData.textureStructure.getFieldValue("type")).intValue();
if (type == TextureHelper.TEX_MAGIC) {
return true;
}
if (type == TextureHelper.TEX_VORONOI) {
int voronoiColorType = ((Number) textureData.textureStructure.getFieldValue("vn_coltype")).intValue();
return voronoiColorType != 0;// voronoiColorType == 0:
// intensity, voronoiColorType
// != 0: col1, col2 or col3
}
if (type == TextureHelper.TEX_CLOUDS) {
int sType = ((Number) textureData.textureStructure.getFieldValue("stype")).intValue();
return sType == 1;// sType==0: without colors, sType==1: with
// colors
}
// checking the flat textures for alpha values presence
if (type == TextureHelper.TEX_IMAGE) {
Image image = textureData.texture.getImage();
switch (image.getFormat()) {
case BGR8:
case DXT1:
case Luminance16F:
case Luminance32F:
case Luminance8:
case RGB111110F:
case RGB16F:
case RGB32F:
case RGB565:
case RGB8:
return true;// these types have no alpha by definition
case ABGR8:
case DXT1A:
case DXT3:
case DXT5:
case Luminance16FAlpha16F:
case Luminance8Alpha8:
case RGBA16F:
case RGBA32F:
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());
TexturePixel pixel = new TexturePixel();
int depth = image.getDepth() == 0 ? 1 : image.getDepth();
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) {
for (int x = 0; x < image.getWidth(); ++x) {
for (int y = 0; y < image.getHeight(); ++y) {
pixelInputOutput.read(image, layerIndex, pixel, x, y);
if (pixel.alpha < 1.0f) {
return false;
}
}
}
}
return true;
default:
throw new IllegalStateException("Unknown image format: " + image.getFormat());
}
}
}
return false;
}
/**
* This method scales the given texture to the given size.
*
* @param texture
* the texture to be scaled
* @param width
* new width of the texture
* @param height
* new height of the texture
*/
private void scale(Texture2D texture, int width, int height) {
// first determine if scaling is required
boolean scaleRequired = texture.getImage().getWidth() != width || texture.getImage().getHeight() != height;
if (scaleRequired) {
Image image = texture.getImage();
BufferedImage sourceImage = ImageToAwt.convert(image, false, true, 0);
int sourceWidth = sourceImage.getWidth();
int sourceHeight = sourceImage.getHeight();
BufferedImage targetImage = new BufferedImage(width, height, sourceImage.getType());
Graphics2D g = targetImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(sourceImage, 0, 0, width, height, 0, 0, sourceWidth, sourceHeight, null);
g.dispose();
Image output = new ImageLoader().load(targetImage, false);
image.setWidth(width);
image.setHeight(height);
image.setData(output.getData(0));
image.setFormat(output.getFormat());
}
}
/**
* A simple class to aggregate the texture data (improves code quality).
*
* @author Marcin Roguski (Kaelthas)
*/
private static class TextureData {
/** The texture. */
public Texture texture;
/** The texture blender (to mix the texture with its material color). */
public TextureBlender textureBlender;
/** The type of UV coordinates. */
public UVCoordinatesType uvCoordinatesType;
/** The type of UV coordinates projection (for flat textures). */
public UVProjectionType projectionType;
/** The texture sructure. */
public Structure textureStructure;
/** The name of the user's UV coordinates that are used for this texture. */
public String uvCoordinatesName;
}
}

@ -0,0 +1,157 @@
package com.jme3.scene.plugins.blender.textures;
import com.jme3.math.FastMath;
import com.jme3.texture.Image.Format;
/**
* The data that helps in bytes calculations for the result image.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class DDSTexelData {
/** The colors of the texes. */
private TexturePixel[][] colors;
/** The indexes of the texels. */
private long[] indexes;
/** The alphas of the texels (might be null). */
private float[][] alphas;
/** The indexels of texels alpha values (might be null). */
private long[] alphaIndexes;
/** The counter of texel x column. */
private int xCounter;
/** The counter of texel y row. */
private int yCounter;
/** The width of the image in pixels. */
private int widthInPixels;
/** The height of the image in pixels. */
private int heightInPixels;
/** The total texel count. */
private int xTexelCount;
/**
* Constructor. Allocates memory for data structures.
*
* @param compressedSize
* the size of compressed image (or its mipmap)
* @param widthToHeightRatio
* width/height ratio for the image
* @param format
* the format of the image
*/
public DDSTexelData(int compressedSize, float widthToHeightRatio, Format format) {
int texelsCount = compressedSize * 8 / format.getBitsPerPixel() / 16;
this.colors = new TexturePixel[texelsCount][];
this.indexes = new long[texelsCount];
this.widthInPixels = (int) (0.5f * (float) Math.sqrt(this.getSizeInBytes() / widthToHeightRatio));
this.heightInPixels = (int) (this.widthInPixels / widthToHeightRatio);
this.xTexelCount = widthInPixels >> 2;
this.yCounter = (heightInPixels >> 2) - 1;// xCounter is 0 for now
if (format == Format.DXT3 || format == Format.DXT5) {
this.alphas = new float[texelsCount][];
this.alphaIndexes = new long[texelsCount];
}
}
/**
* This method adds a color and indexes for a texel.
*
* @param colors
* the colors of the texel
* @param indexes
* the indexes of the texel
*/
public void add(TexturePixel[] colors, int indexes) {
this.add(colors, indexes, null, 0);
}
/**
* This method adds a color, color indexes and alha values (with their
* indexes) for a texel.
*
* @param colors
* the colors of the texel
* @param indexes
* the indexes of the texel
* @param alphas
* the alpha values
* @param alphaIndexes
* the indexes of the given alpha values
*/
public void add(TexturePixel[] colors, int indexes, float[] alphas, long alphaIndexes) {
int index = yCounter * xTexelCount + xCounter;
this.colors[index] = colors;
this.indexes[index] = indexes;
if (alphas != null) {
this.alphas[index] = alphas;
this.alphaIndexes[index] = alphaIndexes;
}
++this.xCounter;
if (this.xCounter >= this.xTexelCount) {
this.xCounter = 0;
--this.yCounter;
}
}
/**
* This method returns the values of the pixel located on the given
* coordinates on the result image.
*
* @param x
* the x coordinate of the pixel
* @param y
* the y coordinate of the pixel
* @param result
* the table where the result is stored
* @return <b>true</b> if the pixel was correctly read and <b>false</b> if
* the position was outside the image sizes
*/
public boolean getRGBA8(int x, int y, byte[] result) {
int xTexetlIndex = x % widthInPixels / 4;
int yTexelIndex = y % heightInPixels / 4;
int texelIndex = yTexelIndex * xTexelCount + xTexetlIndex;
if (texelIndex < colors.length) {
TexturePixel[] colors = this.colors[texelIndex];
// coordinates of the pixel in the selected texel
x = x - 4 * xTexetlIndex;// pixels are arranged from left to right
y = 3 - y - 4 * yTexelIndex;// pixels are arranged from bottom to top (that is why '3 - ...' is at the start)
int pixelIndexInTexel = (y * 4 + x) * (int) FastMath.log(colors.length, 2);
int alphaIndexInTexel = alphas != null ? (y * 4 + x) * (int) FastMath.log(alphas.length, 2) : 0;
// getting the pixel
int indexMask = colors.length - 1;
int colorIndex = (int) (this.indexes[texelIndex] >> pixelIndexInTexel & indexMask);
float alpha = this.alphas != null ? this.alphas[texelIndex][(int) (this.alphaIndexes[texelIndex] >> alphaIndexInTexel & 0x07)] : colors[colorIndex].alpha;
result[0] = (byte) (colors[colorIndex].red * 255.0f);
result[1] = (byte) (colors[colorIndex].green * 255.0f);
result[2] = (byte) (colors[colorIndex].blue * 255.0f);
result[3] = (byte) (alpha * 255.0f);
return true;
}
return false;
}
/**
* @return the size of the decompressed texel (in bytes)
*/
public int getSizeInBytes() {
// indexes.length == count of texels
return indexes.length * 16 * 4;
}
/**
* @return image (mipmap) width
*/
public int getPixelWidth() {
return widthInPixels;
}
/**
* @return image (mipmap) height
*/
public int getPixelHeight() {
return heightInPixels;
}
}

@ -0,0 +1,282 @@
package com.jme3.scene.plugins.blender.textures;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import com.jme3.bounding.BoundingBox;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.textures.TriangulatedTexture.TriangleTextureElement;
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType;
import com.jme3.scene.plugins.blender.textures.generating.TextureGenerator;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.TextureCubeMap;
import com.jme3.util.TempVars;
/**
* The generated texture loaded from blender file. The texture is not generated
* after being read. This class rather stores all required data and can compute
* a pixel in the required 3D space position.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class GeneratedTexture extends Texture {
private static final int POSITIVE_X = 0;
private static final int NEGATIVE_X = 1;
private static final int POSITIVE_Y = 2;
private static final int NEGATIVE_Y = 3;
private static final int POSITIVE_Z = 4;
private static final int NEGATIVE_Z = 5;
// flag values
public static final int TEX_COLORBAND = 1;
public static final int TEX_FLIPBLEND = 2;
public static final int TEX_NEGALPHA = 4;
public static final int TEX_CHECKER_ODD = 8;
public static final int TEX_CHECKER_EVEN = 16;
public static final int TEX_PRV_ALPHA = 32;
public static final int TEX_PRV_NOR = 64;
public static final int TEX_REPEAT_XMIR = 128;
public static final int TEX_REPEAT_YMIR = 256;
public static final int TEX_FLAG_MASK = TEX_COLORBAND | TEX_FLIPBLEND | TEX_NEGALPHA | TEX_CHECKER_ODD | TEX_CHECKER_EVEN | TEX_PRV_ALPHA | TEX_PRV_NOR | TEX_REPEAT_XMIR | TEX_REPEAT_YMIR;
/** Material-texture link structure. */
private final Structure mTex;
/** Texture generateo for the specified texture type. */
private final TextureGenerator textureGenerator;
/**
* The generated texture cast functions. They are used to cas a given point on a plane to a specified shape in 3D space.
* The functions should be ordered as the ordinal of a BlenderKey.CastFunction enums.
*/
private final static CastFunction[] CAST_FUNCTIONS = new CastFunction[] {
/**
* The cube casting function (does nothing except scaling if needed because the given points are already on a cube).
*/
new CastFunction() {
@Override
public void cast(Vector3f pointToCast, float radius) {
//computed using the Thales' theorem
float length = 2 * pointToCast.subtractLocal(0.5f, 0.5f, 0.5f).length() * radius;
pointToCast.normalizeLocal().addLocal(0.5f, 0.5f, 0.5f).multLocal(length);
}
},
/**
* The sphere casting function.
*/
new CastFunction() {
/**
* The method casts a point on a plane to a sphere.
* The plane is one of the faces of a cube that has a edge of length 1 and center in (0.5 0.5, 0.5). This cube is a basic 3d area where generated texture
* is created.
* To cast a point on a cube face to a sphere that is inside the cube we perform several easy vector operations.
* 1. create a vector from the cube's center to the point
* 2. setting its length to 0.5 (the radius of the sphere)
* 3. adding the value of the cube's center to get a point on the sphere
*
* The result is stored in the given vector.
*
* @param pointToCast
* the point on a plane that will be cast to a sphere
* @param radius
* the radius of the sphere
*/
@Override
public void cast(Vector3f pointToCast, float radius) {
pointToCast.subtractLocal(0.5f, 0.5f, 0.5f).normalizeLocal().multLocal(radius).addLocal(0.5f, 0.5f, 0.5f);
}
}
};
/**
* Constructor. Reads the required data from the 'tex' structure.
*
* @param tex
* the texture structure
* @param mTex
* the material-texture link data structure
* @param textureGenerator
* the generator for the required texture type
* @param blenderContext
* the blender context
*/
public GeneratedTexture(Structure tex, Structure mTex, TextureGenerator textureGenerator, BlenderContext blenderContext) {
this.mTex = mTex;
this.textureGenerator = textureGenerator;
this.textureGenerator.readData(tex, blenderContext);
super.setImage(new GeneratedTextureImage(textureGenerator.getImageFormat()));
}
/**
* This method computes the textyre color/intensity at the specified (u, v,
* s) position in 3D space.
*
* @param pixel
* the pixel where the result is stored
* @param u
* the U factor
* @param v
* the V factor
* @param s
* the S factor
*/
public void getPixel(TexturePixel pixel, float u, float v, float s) {
textureGenerator.getPixel(pixel, u, v, s);
}
/**
* This method triangulates the texture. In the result we get a set of small
* flat textures for each face of the given mesh. This can be later merged
* into one flat texture.
*
* @param mesh
* the mesh we create the texture for
* @param geometriesOMA
* the old memory address of the geometries group that the given
* mesh belongs to (required for bounding box calculations)
* @param coordinatesType
* the types of UV coordinates
* @param blenderContext
* the blender context
* @return triangulated texture
*/
public TriangulatedTexture triangulate(Mesh mesh, Long geometriesOMA, UVCoordinatesType coordinatesType, BlenderContext blenderContext) {
TemporalMesh geometries = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH);
int[] coordinatesSwappingIndexes = new int[] { ((Number) mTex.getFieldValue("projx")).intValue(), ((Number) mTex.getFieldValue("projy")).intValue(), ((Number) mTex.getFieldValue("projz")).intValue() };
List<Vector3f> uvs = UVCoordinatesGenerator.generateUVCoordinatesFor3DTexture(mesh, coordinatesType, coordinatesSwappingIndexes, geometries);
Vector3f[] uvsArray = uvs.toArray(new Vector3f[uvs.size()]);
BoundingBox boundingBox = UVCoordinatesGenerator.getBoundingBox(geometries);
Set<TriangleTextureElement> triangleTextureElements = new TreeSet<TriangleTextureElement>(new Comparator<TriangleTextureElement>() {
@Override
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
return o1.faceIndex - o2.faceIndex;
}
});
int[] indices = new int[3];
for (int i = 0; i < mesh.getTriangleCount(); ++i) {
mesh.getTriangle(i, indices);
triangleTextureElements.add(new TriangleTextureElement(i, boundingBox, this, uvsArray, indices, blenderContext));
}
return new TriangulatedTexture(triangleTextureElements, blenderContext);
}
/**
* Creates a texture for the sky. The result texture has 6 layers.
* @param size
* the size of the texture (width and height are equal)
* @param horizontalColor
* the horizon color
* @param zenithColor
* the zenith color
* @param blenderContext
* the blender context
* @return the sky texture
*/
public TextureCubeMap generateSkyTexture(int size, ColorRGBA horizontalColor, ColorRGBA zenithColor, BlenderContext blenderContext) {
Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6);
PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat());
TexturePixel pixel = new TexturePixel();
float delta = 1 / (float) (size - 1);
float sideV, sideS = 1, forwardU = 1, forwardV, upS;
TempVars tempVars = TempVars.get();
CastFunction castFunction = CAST_FUNCTIONS[blenderContext.getBlenderKey().getSkyGeneratedTextureShape().ordinal()];
float castRadius = blenderContext.getBlenderKey().getSkyGeneratedTextureRadius();
for (int x = 0; x < size; ++x) {
sideV = 1;
forwardV = 1;
upS = 0;
for (int y = 0; y < size; ++y) {
castFunction.cast(tempVars.vect1.set(1, sideV, sideS), castRadius);
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
pixelIO.write(image, NEGATIVE_X, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// right
castFunction.cast(tempVars.vect1.set(0, sideV, 1 - sideS), castRadius);
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
pixelIO.write(image, POSITIVE_X, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// left
castFunction.cast(tempVars.vect1.set(forwardU, forwardV, 0), castRadius);
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
pixelIO.write(image, POSITIVE_Z, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// front
castFunction.cast(tempVars.vect1.set(1 - forwardU, forwardV, 1), castRadius);
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
pixelIO.write(image, NEGATIVE_Z, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// back
castFunction.cast(tempVars.vect1.set(forwardU, 0, upS), castRadius);
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
pixelIO.write(image, NEGATIVE_Y, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// top
castFunction.cast(tempVars.vect1.set(forwardU, 1, 1 - upS), castRadius);
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z);
pixelIO.write(image, POSITIVE_Y, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// bottom
sideV = FastMath.clamp(sideV - delta, 0, 1);
forwardV = FastMath.clamp(forwardV - delta, 0, 1);
upS = FastMath.clamp(upS + delta, 0, 1);
}
sideS = FastMath.clamp(sideS - delta, 0, 1);
forwardU = FastMath.clamp(forwardU - delta, 0, 1);
}
tempVars.release();
return new TextureCubeMap(image);
}
@Override
public void setWrap(WrapAxis axis, WrapMode mode) {
}
@Override
public void setWrap(WrapMode mode) {
}
@Override
public WrapMode getWrap(WrapAxis axis) {
return null;
}
@Override
public Type getType() {
return Type.ThreeDimensional;
}
@Override
public Texture createSimpleClone() {
return null;
}
/**
* Private class to give the format of the 'virtual' 3D texture image.
*
* @author Marcin Roguski (Kaelthas)
*/
private static class GeneratedTextureImage extends Image {
public GeneratedTextureImage(Format imageFormat) {
super.format = imageFormat;
}
}
/**
* The casting functions to create a sky generated texture against selected shape of a selected size.
*
* @author Marcin Roguski (Kaelthas)
*/
private static interface CastFunction {
void cast(Vector3f pointToCast, float radius);
}
}

@ -0,0 +1,136 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures;
import com.jme3.asset.AssetManager;
import com.jme3.asset.TextureKey;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.plugins.AWTLoader;
import com.jme3.texture.plugins.HDRLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An image loader class. It uses three loaders (AWTLoader, TGALoader and DDSLoader) in an attempt to load the image from the given
* input stream.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ImageLoader extends AWTLoader {
private static final Logger LOGGER = Logger.getLogger(ImageLoader.class.getName());
private static final Logger hdrLogger = Logger.getLogger(HDRLoader.class.getName()); // Used to silence HDR Errors
/**
* List of Blender-Supported Texture Extensions (we have to guess them, so
* the AssetLoader can find them. Not good, but better than nothing.
* Source: https://docs.blender.org/manual/en/dev/data_system/files/media/image_formats.html
*/
private static final String[] extensions = new String[]
{ /* Windows Bitmap */".bmp",
/* Iris */ ".sgi", ".rgb", ".bw",
/* PNG */ ".png",
/* JPEG */ ".jpg", ".jpeg",
/* JPEG 2000 */ ".jp2", ".j2c",
/* Targa */".tga",
/* Cineon & DPX */".cin", ".dpx",
/* OpenEXR */ ".exr",
/* Radiance HDR */ ".hdr",
/* TIFF */ ".tif", ".tiff",
/* DDS (Direct X) */ ".dds" };
/**
* This method loads a image which is packed into the blender file.
* It makes use of all the registered AssetLoaders
*
* @param inputStream
* blender input stream
* @param startPosition
* position in the stream where the image data starts
* @param flipY
* if the image should be flipped (does not work with DirectX image)
* @return loaded image or null if it could not be loaded
* @deprecated This method has only been left in for API compability.
* Use loadTexture instead
*/
public Image loadImage(AssetManager assetManager, BlenderInputStream inputStream, int startPosition, boolean flipY) {
Texture tex = loadTexture(assetManager, inputStream, startPosition, flipY);
if (tex == null) {
return null;
} else {
return tex.getImage();
}
}
/**
* This method loads a texture which is packed into the blender file.
* It makes use of all the registered AssetLoaders
*
* @param inputStream
* blender input stream
* @param startPosition
* position in the stream where the image data starts
* @param flipY
* if the image should be flipped (does not work with DirectX image)
* @return loaded texture or null if it could not be loaded
*/
public Texture loadTexture(AssetManager assetManager, BlenderInputStream inputStream, int startPosition, boolean flipY) {
inputStream.setPosition(startPosition);
TextureKey tKey;
Texture result = null;
hdrLogger.setLevel(Level.SEVERE); // When we bruteforce try HDR on a non hdr file, it prints unreadable chars
for (String ext: extensions) {
tKey = new TextureKey("dummy" + ext, flipY);
try {
result = assetManager.loadAssetFromStream(tKey, inputStream);
} catch (Exception e) {
continue;
}
if (result != null) {
break; // Could locate a possible asset
}
}
if (result == null) {
LOGGER.warning("Texture could not be loaded by any of the available loaders!\n"
+ "Since the file has been packed into the blender file, there is no"
+ "way for us to tell you which texture it was.");
}
return result;
}
}

@ -0,0 +1,473 @@
package com.jme3.scene.plugins.blender.textures;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import jme3tools.converters.ImageToAwt;
import jme3tools.converters.RGB565;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;
/**
* This utility class has the methods that deal with images.
*
* @author Marcin Roguski (Kaelthas)
*/
public final class ImageUtils {
/**
* Creates an image of the given size and depth.
* @param format
* the image format
* @param width
* the image width
* @param height
* the image height
* @param depth
* the image depth
* @return the new image instance
*/
public static Image createEmptyImage(Format format, int width, int height, int depth) {
int bufferSize = width * height * (format.getBitsPerPixel() >> 3);
if (depth < 2) {
return new Image(format, width, height, BufferUtils.createByteBuffer(bufferSize), com.jme3.texture.image.ColorSpace.Linear);
}
ArrayList<ByteBuffer> data = new ArrayList<ByteBuffer>(depth);
for (int i = 0; i < depth; ++i) {
data.add(BufferUtils.createByteBuffer(bufferSize));
}
return new Image(Format.RGB8, width, height, depth, data, com.jme3.texture.image.ColorSpace.Linear);
}
/**
* The method sets a color for the given pixel by merging the two given colors.
* The lowIntensityColor will be most visible when the pixel has low intensity.
* The highIntensityColor will be most visible when the pixel has high intensity.
*
* @param pixel
* the pixel that will have the colors altered
* @param lowIntensityColor
* the low intensity color
* @param highIntensityColor
* the high intensity color
* @return the altered pixel (the same instance)
*/
public static TexturePixel color(TexturePixel pixel, ColorRGBA lowIntensityColor, ColorRGBA highIntensityColor) {
float intensity = pixel.intensity;
pixel.fromColor(lowIntensityColor);
pixel.mult(1 - pixel.intensity);
pixel.add(highIntensityColor.mult(intensity));
return pixel;
}
/**
* This method merges two given images. The result is stored in the
* 'target' image.
*
* @param targetImage
* the target image
* @param sourceImage
* the source image
*/
public static void merge(Image targetImage, Image sourceImage) {
if (sourceImage.getDepth() != targetImage.getDepth()) {
throw new IllegalArgumentException("The given images should have the same depth to merge them!");
}
if (sourceImage.getWidth() != targetImage.getWidth()) {
throw new IllegalArgumentException("The given images should have the same width to merge them!");
}
if (sourceImage.getHeight() != targetImage.getHeight()) {
throw new IllegalArgumentException("The given images should have the same height to merge them!");
}
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(sourceImage.getFormat());
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(targetImage.getFormat());
TexturePixel sourcePixel = new TexturePixel();
TexturePixel targetPixel = new TexturePixel();
int depth = targetImage.getDepth() == 0 ? 1 : targetImage.getDepth();
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) {
for (int x = 0; x < sourceImage.getWidth(); ++x) {
for (int y = 0; y < sourceImage.getHeight(); ++y) {
sourceIO.read(sourceImage, layerIndex, sourcePixel, x, y);
targetIO.read(targetImage, layerIndex, targetPixel, x, y);
targetPixel.merge(sourcePixel);
targetIO.write(targetImage, layerIndex, targetPixel, x, y);
}
}
}
}
/**
* This method merges two given images. The result is stored in the
* 'target' image.
*
* @param targetImage
* the target image
* @param sourceImage
* the source image
*/
public static void mix(Image targetImage, Image sourceImage) {
if (sourceImage.getDepth() != targetImage.getDepth()) {
throw new IllegalArgumentException("The given images should have the same depth to merge them!");
}
if (sourceImage.getWidth() != targetImage.getWidth()) {
throw new IllegalArgumentException("The given images should have the same width to merge them!");
}
if (sourceImage.getHeight() != targetImage.getHeight()) {
throw new IllegalArgumentException("The given images should have the same height to merge them!");
}
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(sourceImage.getFormat());
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(targetImage.getFormat());
TexturePixel sourcePixel = new TexturePixel();
TexturePixel targetPixel = new TexturePixel();
int depth = targetImage.getDepth() == 0 ? 1 : targetImage.getDepth();
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) {
for (int x = 0; x < sourceImage.getWidth(); ++x) {
for (int y = 0; y < sourceImage.getHeight(); ++y) {
sourceIO.read(sourceImage, layerIndex, sourcePixel, x, y);
targetIO.read(targetImage, layerIndex, targetPixel, x, y);
targetPixel.mix(sourcePixel);
targetIO.write(targetImage, layerIndex, targetPixel, x, y);
}
}
}
}
/**
* Resizes the image to the given width and height.
* @param source
* the source image (this remains untouched, the new image instance is created)
* @param width
* the target image width
* @param height
* the target image height
* @return the resized image
*/
public static Image resizeTo(Image source, int width, int height) {
BufferedImage sourceImage = ImageToAwt.convert(source, false, false, 0);
double scaleX = width / (double) sourceImage.getWidth();
double scaleY = height / (double) sourceImage.getHeight();
AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY);
AffineTransformOp bilinearScaleOp = new AffineTransformOp(scaleTransform, AffineTransformOp.TYPE_BILINEAR);
BufferedImage scaledImage = bilinearScaleOp.filter(sourceImage, new BufferedImage(width, height, sourceImage.getType()));
return ImageUtils.toJmeImage(scaledImage, source.getFormat());
}
/**
* This method converts the given texture into normal-map texture.
*
* @param source
* the source texture
* @param strengthFactor
* the normal strength factor
* @return normal-map texture
*/
public static Image convertToNormalMapTexture(Image source, float strengthFactor) {
BufferedImage sourceImage = ImageToAwt.convert(source, false, false, 0);
BufferedImage heightMap = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
BufferedImage bumpMap = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
ColorConvertOp gscale = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
gscale.filter(sourceImage, heightMap);
Vector3f S = new Vector3f();
Vector3f T = new Vector3f();
Vector3f N = new Vector3f();
for (int x = 0; x < bumpMap.getWidth(); ++x) {
for (int y = 0; y < bumpMap.getHeight(); ++y) {
// generating bump pixel
S.x = 1;
S.y = 0;
S.z = strengthFactor * ImageUtils.getHeight(heightMap, x + 1, y) - strengthFactor * ImageUtils.getHeight(heightMap, x - 1, y);
T.x = 0;
T.y = 1;
T.z = strengthFactor * ImageUtils.getHeight(heightMap, x, y + 1) - strengthFactor * ImageUtils.getHeight(heightMap, x, y - 1);
float den = (float) Math.sqrt(S.z * S.z + T.z * T.z + 1);
N.x = -S.z;
N.y = -T.z;
N.z = 1;
N.divideLocal(den);
// setting the pixel in the result image
bumpMap.setRGB(x, y, ImageUtils.vectorToColor(N.x, N.y, N.z));
}
}
return ImageUtils.toJmeImage(bumpMap, source.getFormat());
}
/**
* This method converts the given texture into black and whit (grayscale) texture.
*
* @param source
* the source texture
* @return grayscale texture
*/
public static Image convertToGrayscaleTexture(Image source) {
BufferedImage sourceImage = ImageToAwt.convert(source, false, false, 0);
ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
op.filter(sourceImage, sourceImage);
return ImageUtils.toJmeImage(sourceImage, source.getFormat());
}
/**
* This method decompresses the given image. If the given image is already
* decompressed nothing happens and it is simply returned.
*
* @param image
* the image to decompress
* @return the decompressed image
*/
public static Image decompress(Image image) {
Format format = image.getFormat();
int depth = image.getDepth();
if (depth == 0) {
depth = 1;
}
ArrayList<ByteBuffer> dataArray = new ArrayList<ByteBuffer>(depth);
int[] sizes = image.getMipMapSizes() != null ? image.getMipMapSizes() : new int[1];
int[] newMipmapSizes = image.getMipMapSizes() != null ? new int[image.getMipMapSizes().length] : null;
for (int dataLayerIndex = 0; dataLayerIndex < depth; ++dataLayerIndex) {
ByteBuffer data = image.getData(dataLayerIndex);
data.rewind();
if (sizes.length == 1) {
sizes[0] = data.remaining();
}
float widthToHeightRatio = image.getWidth() / image.getHeight();// this should always be constant for each mipmap
List<DDSTexelData> texelDataList = new ArrayList<DDSTexelData>(sizes.length);
int maxPosition = 0, resultSize = 0;
for (int sizeIndex = 0; sizeIndex < sizes.length; ++sizeIndex) {
maxPosition += sizes[sizeIndex];
DDSTexelData texelData = new DDSTexelData(sizes[sizeIndex], widthToHeightRatio, format);
texelDataList.add(texelData);
switch (format) {
case DXT1:// BC1
case DXT1A:
while (data.position() < maxPosition) {
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel(), new TexturePixel(), new TexturePixel() };
short c0 = data.getShort();
short c1 = data.getShort();
int col0 = RGB565.RGB565_to_ARGB8(c0);
int col1 = RGB565.RGB565_to_ARGB8(c1);
colors[0].fromARGB8(col0);
colors[1].fromARGB8(col1);
if (col0 > col1) {
// creating color2 = 2/3color0 + 1/3color1
colors[2].fromPixel(colors[0]);
colors[2].mult(2);
colors[2].add(colors[1]);
colors[2].divide(3);
// creating color3 = 1/3color0 + 2/3color1;
colors[3].fromPixel(colors[1]);
colors[3].mult(2);
colors[3].add(colors[0]);
colors[3].divide(3);
} else {
// creating color2 = 1/2color0 + 1/2color1
colors[2].fromPixel(colors[0]);
colors[2].add(colors[1]);
colors[2].mult(0.5f);
colors[3].fromARGB8(0);
}
int indexes = data.getInt();// 4-byte table with color indexes in decompressed table
texelData.add(colors, indexes);
}
break;
case DXT3:// BC2
while (data.position() < maxPosition) {
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel(), new TexturePixel(), new TexturePixel() };
long alpha = data.getLong();
float[] alphas = new float[16];
long alphasIndex = 0;
for (int i = 0; i < 16; ++i) {
alphasIndex |= i << i * 4;
byte a = (byte) ((alpha >> i * 4 & 0x0F) << 4);
alphas[i] = a >= 0 ? a / 255.0f : 1.0f - ~a / 255.0f;
}
short c0 = data.getShort();
short c1 = data.getShort();
int col0 = RGB565.RGB565_to_ARGB8(c0);
int col1 = RGB565.RGB565_to_ARGB8(c1);
colors[0].fromARGB8(col0);
colors[1].fromARGB8(col1);
// creating color2 = 2/3color0 + 1/3color1
colors[2].fromPixel(colors[0]);
colors[2].mult(2);
colors[2].add(colors[1]);
colors[2].divide(3);
// creating color3 = 1/3color0 + 2/3color1;
colors[3].fromPixel(colors[1]);
colors[3].mult(2);
colors[3].add(colors[0]);
colors[3].divide(3);
int indexes = data.getInt();// 4-byte table with color indexes in decompressed table
texelData.add(colors, indexes, alphas, alphasIndex);
}
break;
case DXT5:// BC3
float[] alphas = new float[8];
while (data.position() < maxPosition) {
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel(), new TexturePixel(), new TexturePixel() };
alphas[0] = 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
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.
alphas[2] = (6 * alphas[0] + alphas[1]) / 7;
alphas[3] = (5 * alphas[0] + 2 * alphas[1]) / 7;
alphas[4] = (4 * alphas[0] + 3 * alphas[1]) / 7;
alphas[5] = (3 * alphas[0] + 4 * alphas[1]) / 7;
alphas[6] = (2 * alphas[0] + 5 * alphas[1]) / 7;
alphas[7] = (alphas[0] + 6 * alphas[1]) / 7;
} else {
alphas[2] = (4 * alphas[0] + alphas[1]) * 0.2f;
alphas[3] = (3 * alphas[0] + 2 * alphas[1]) * 0.2f;
alphas[4] = (2 * alphas[0] + 3 * alphas[1]) * 0.2f;
alphas[5] = (alphas[0] + 4 * alphas[1]) * 0.2f;
alphas[6] = 0;
alphas[7] = 1;
}
short c0 = data.getShort();
short c1 = data.getShort();
int col0 = RGB565.RGB565_to_ARGB8(c0);
int col1 = RGB565.RGB565_to_ARGB8(c1);
colors[0].fromARGB8(col0);
colors[1].fromARGB8(col1);
// creating color2 = 2/3color0 + 1/3color1
colors[2].fromPixel(colors[0]);
colors[2].mult(2);
colors[2].add(colors[1]);
colors[2].divide(3);
// creating color3 = 1/3color0 + 2/3color1;
colors[3].fromPixel(colors[1]);
colors[3].mult(2);
colors[3].add(colors[0]);
colors[3].divide(3);
int indexes = data.getInt();// 4-byte table with color indexes in decompressed table
texelData.add(colors, indexes, alphas, alphaIndices);
}
break;
default:
throw new IllegalStateException("Unknown compressed format: " + format);
}
newMipmapSizes[sizeIndex] = texelData.getSizeInBytes();
resultSize += texelData.getSizeInBytes();
}
byte[] bytes = new byte[resultSize];
int offset = 0;
byte[] pixelBytes = new byte[4];
for (DDSTexelData texelData : texelDataList) {
for (int i = 0; i < texelData.getPixelWidth(); ++i) {
for (int j = 0; j < texelData.getPixelHeight(); ++j) {
if (texelData.getRGBA8(i, j, pixelBytes)) {
bytes[offset + (j * texelData.getPixelWidth() + i) * 4] = pixelBytes[0];
bytes[offset + (j * texelData.getPixelWidth() + i) * 4 + 1] = pixelBytes[1];
bytes[offset + (j * texelData.getPixelWidth() + i) * 4 + 2] = pixelBytes[2];
bytes[offset + (j * texelData.getPixelWidth() + i) * 4 + 3] = pixelBytes[3];
} else {
break;
}
}
}
offset += texelData.getSizeInBytes();
}
dataArray.add(BufferUtils.createByteBuffer(bytes));
}
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) {
result.setMipMapSizes(newMipmapSizes);
}
return result;
}
/**
* This method returns the height represented by the specified pixel in the
* given texture. The given texture should be a height-map.
*
* @param image
* the height-map texture
* @param x
* pixel's X coordinate
* @param y
* pixel's Y coordinate
* @return height represented by the given texture in the specified location
*/
private static int getHeight(BufferedImage image, int x, int y) {
if (x < 0) {
x = 0;
} else if (x >= image.getWidth()) {
x = image.getWidth() - 1;
}
if (y < 0) {
y = 0;
} else if (y >= image.getHeight()) {
y = image.getHeight() - 1;
}
return image.getRGB(x, y) & 0xff;
}
/**
* This method transforms given vector's coordinates into ARGB color (A is
* always = 255).
*
* @param x
* X factor of the vector
* @param y
* Y factor of the vector
* @param z
* Z factor of the vector
* @return color representation of the given vector
*/
private static int vectorToColor(float x, float y, float z) {
int r = Math.round(255 * (x + 1f) / 2f);
int g = Math.round(255 * (y + 1f) / 2f);
int b = Math.round(255 * (z + 1f) / 2f);
return (255 << 24) + (r << 16) + (g << 8) + b;
}
/**
* Converts java awt image to jme image.
* @param bufferedImage
* the java awt image
* @param format
* the result image format
* @return the jme image
*/
private static Image toJmeImage(BufferedImage bufferedImage, Format format) {
ByteBuffer byteBuffer = BufferUtils.createByteBuffer(bufferedImage.getWidth() * bufferedImage.getHeight() * 3);
ImageToAwt.convert(bufferedImage, format, byteBuffer);
return new Image(format, bufferedImage.getWidth(), bufferedImage.getHeight(), byteBuffer, com.jme3.texture.image.ColorSpace.Linear);
}
}

@ -0,0 +1,691 @@
/*
* Copyright (c) 2009-2018 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures;
import java.awt.geom.AffineTransform;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetManager;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.asset.BlenderKey;
import com.jme3.asset.GeneratedTextureKey;
import com.jme3.asset.TextureKey;
import com.jme3.math.Vector2f;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlenderFactory;
import com.jme3.scene.plugins.blender.textures.generating.TextureGeneratorFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.MinFilter;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import com.jme3.util.PlaceholderAssets;
/**
* A class that is used in texture calculations.
*
* @author Marcin Roguski
*/
public class TextureHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(TextureHelper.class.getName());
// texture types
public static final int TEX_NONE = 0;
public static final int TEX_CLOUDS = 1;
public static final int TEX_WOOD = 2;
public static final int TEX_MARBLE = 3;
public static final int TEX_MAGIC = 4;
public static final int TEX_BLEND = 5;
public static final int TEX_STUCCI = 6;
public static final int TEX_NOISE = 7;
public static final int TEX_IMAGE = 8;
public static final int TEX_PLUGIN = 9;
public static final int TEX_ENVMAP = 10;
public static final int TEX_MUSGRAVE = 11;
public static final int TEX_VORONOI = 12;
public static final int TEX_DISTNOISE = 13;
public static final int TEX_POINTDENSITY = 14; // v. 25+
public static final int TEX_VOXELDATA = 15; // v. 25+
public static final int TEX_OCEAN = 16; // v. 26+
public static final Type[] TEXCOORD_TYPES = new Type[] { Type.TexCoord, Type.TexCoord2, Type.TexCoord3, Type.TexCoord4, Type.TexCoord5, Type.TexCoord6, Type.TexCoord7, Type.TexCoord8 };
private TextureGeneratorFactory textureGeneratorFactory = new TextureGeneratorFactory();
/**
* This constructor parses the given blender version and stores the result.
* It creates noise generator and texture generators.
*
* @param blenderVersion
* the version read from the blend file
* @param blenderContext
* the blender context
*/
public TextureHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext);
}
/**
* This class returns a texture read from the file or from packed blender
* data. The returned texture has the name set to the value of its blender
* type.
*
* @param textureStructure
* texture structure filled with data
* @param blenderContext
* the blender context
* @return the texture that can be used by JME engine
* @throws BlenderFileException
* this exception is thrown when the blend file structure is
* somehow invalid or corrupted
*/
public Texture getTexture(Structure textureStructure, Structure mTex, BlenderContext blenderContext) throws BlenderFileException {
Texture result = (Texture) blenderContext.getLoadedFeature(textureStructure.getOldMemoryAddress(), LoadedDataType.FEATURE);
if (result != null) {
return result;
}
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) {
case TEX_IMAGE:// (it is first because probably this will be most commonly used)
Pointer pImage = (Pointer) textureStructure.getFieldValue("ima");
if (pImage.isNotNull()) {
Structure image = pImage.fetchData().get(0);
Texture loadedTexture = this.loadImageAsTexture(image, imaflag, blenderContext);
if (loadedTexture != null) {
result = loadedTexture;
this.applyColorbandAndColorFactors(textureStructure, result.getImage(), blenderContext);
}
}
break;
case TEX_CLOUDS:
case TEX_WOOD:
case TEX_MARBLE:
case TEX_MAGIC:
case TEX_BLEND:
case TEX_STUCCI:
case TEX_NOISE:
case TEX_MUSGRAVE:
case TEX_VORONOI:
case TEX_DISTNOISE:
result = new GeneratedTexture(textureStructure, mTex, textureGeneratorFactory.createTextureGenerator(type), blenderContext);
break;
case TEX_NONE:// No texture, do nothing
break;
case TEX_POINTDENSITY:
case TEX_VOXELDATA:
case TEX_PLUGIN:
case TEX_ENVMAP:
case TEX_OCEAN:
LOGGER.log(Level.WARNING, "Unsupported texture type: {0} for texture: {1}", new Object[] { type, textureStructure.getName() });
break;
default:
throw new BlenderFileException("Unknown texture type: " + type + " for texture: " + textureStructure.getName());
}
if (result != null) {
result.setName(textureStructure.getName());
result.setWrap(WrapMode.Repeat);
// decide if the mipmaps will be generated
switch (blenderContext.getBlenderKey().getMipmapGenerationMethod()) {
case ALWAYS_GENERATE:
result.setMinFilter(MinFilter.Trilinear);
break;
case NEVER_GENERATE:
break;
case GENERATE_WHEN_NEEDED:
if ((imaflag & 0x04) != 0) {
result.setMinFilter(MinFilter.Trilinear);
}
break;
default:
throw new IllegalStateException("Unknown mipmap generation method: " + blenderContext.getBlenderKey().getMipmapGenerationMethod());
}
if (type != TEX_IMAGE) {// only generated textures should have this key
result.setKey(new GeneratedTextureKey(textureStructure.getName()));
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Adding texture {0} to the loaded features with OMA = {1}", new Object[] { result.getName(), textureStructure.getOldMemoryAddress() });
}
blenderContext.addLoadedFeatures(textureStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, textureStructure);
blenderContext.addLoadedFeatures(textureStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result);
}
return result;
}
/**
* This class returns a texture read from the file or from packed blender
* data.
*
* @param imageStructure
* image structure filled with data
* @param imaflag
* the image flag
* @param blenderContext
* the blender context
* @return the texture that can be used by JME engine
* @throws BlenderFileException
* this exception is thrown when the blend file structure is
* somehow invalid or corrupted
*/
public Texture loadImageAsTexture(Structure imageStructure, int imaflag, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Fetching texture with OMA = {0}", imageStructure.getOldMemoryAddress());
Texture result = null;
Image im = (Image) blenderContext.getLoadedFeature(imageStructure.getOldMemoryAddress(), LoadedDataType.FEATURE);
// if (im == null) { HACK force reaload always, as constructor in else case is destroying the TextureKeys!
if ("ID".equals(imageStructure.getType())) {
LOGGER.fine("Loading texture from external blend file.");
result = (Texture) this.loadLibrary(imageStructure);
} else {
String texturePath = imageStructure.getFieldValue("name").toString();
Pointer pPackedFile = (Pointer) imageStructure.getFieldValue("packedfile");
if (pPackedFile.isNull()) {
LOGGER.log(Level.FINE, "Reading texture from file: {0}", texturePath);
result = this.loadImageFromFile(texturePath, imaflag, blenderContext);
} else {
LOGGER.fine("Packed texture. Reading directly from the blend file!");
Structure packedFile = pPackedFile.fetchData().get(0);
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 ImageLoader().loadTexture(blenderContext.getAssetManager(), blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true);
if (result == null) {
result = new Texture2D(PlaceholderAssets.getPlaceholderImage(blenderContext.getAssetManager()));
LOGGER.fine("ImageLoader returned null. It probably failed to load the packed texture, using placeholder asset");
}
}
}
//} else {
// 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;
}
/**
* This method creates the affine transform that is used to transform a
* triangle defined by one UV coordinates into a triangle defined by
* different UV's.
*
* @param source
* source UV coordinates
* @param dest
* target UV coordinates
* @param sourceSize
* the width and height of the source image
* @param targetSize
* the width and height of the target image
* @return affine transform to transform one triangle to another
*/
public AffineTransform createAffineTransform(Vector2f[] source, Vector2f[] dest, int[] sourceSize, int[] targetSize) {
float x11 = source[0].getX() * sourceSize[0];
float x12 = source[0].getY() * sourceSize[1];
float x21 = source[1].getX() * sourceSize[0];
float x22 = source[1].getY() * sourceSize[1];
float x31 = source[2].getX() * sourceSize[0];
float x32 = source[2].getY() * sourceSize[1];
float y11 = dest[0].getX() * targetSize[0];
float y12 = dest[0].getY() * targetSize[1];
float y21 = dest[1].getX() * targetSize[0];
float y22 = dest[1].getY() * targetSize[1];
float y31 = dest[2].getX() * targetSize[0];
float y32 = dest[2].getY() * targetSize[1];
float a1 = ((y11 - y21) * (x12 - x32) - (y11 - y31) * (x12 - x22)) / ((x11 - x21) * (x12 - x32) - (x11 - x31) * (x12 - x22));
float a2 = ((y11 - y21) * (x11 - x31) - (y11 - y31) * (x11 - x21)) / ((x12 - x22) * (x11 - x31) - (x12 - x32) * (x11 - x21));
float a3 = y11 - a1 * x11 - a2 * x12;
float a4 = ((y12 - y22) * (x12 - x32) - (y12 - y32) * (x12 - x22)) / ((x11 - x21) * (x12 - x32) - (x11 - x31) * (x12 - x22));
float a5 = ((y12 - y22) * (x11 - x31) - (y12 - y32) * (x11 - x21)) / ((x12 - x22) * (x11 - x31) - (x12 - x32) * (x11 - x21));
float a6 = y12 - a4 * x11 - a5 * x12;
return new AffineTransform(a1, a4, a2, a5, a3, a6);
}
/**
* This method returns the proper pixel position on the image.
*
* @param pos
* the relative position (value of range [0, 1] (both inclusive))
* @param size
* the size of the line the pixel lies on (width, height or
* depth)
* @return the integer index of the pixel on the line of the specified width
*/
public int getPixelPosition(float pos, int size) {
float pixelWidth = 1 / (float) size;
pos *= size;
int result = (int) pos;
// here is where we repair floating point operations errors :)
if (Math.abs(result - pos) > pixelWidth) {
++result;
}
return result;
}
/**
* This method returns subimage of the give image. The subimage is
* constrained by the rectangle coordinates. The source image is unchanged.
*
* @param image
* the image to be subimaged
* @param minX
* minimum X position
* @param minY
* minimum Y position
* @param maxX
* maximum X position
* @param maxY
* maximum Y position
* @return a part of the given image
*/
public Image getSubimage(Image image, int minX, int minY, int maxX, int maxY) {
if (minY > maxY) {
throw new IllegalArgumentException("Minimum Y value is higher than maximum Y value!");
}
if (minX > maxX) {
throw new IllegalArgumentException("Minimum Y value is higher than maximum Y value!");
}
if (image.getData().size() > 1) {
throw new IllegalArgumentException("Only flat images are allowed for subimage operation!");
}
if (image.getMipMapSizes() != null) {
LOGGER.warning("Subimaging image with mipmaps is not yet supported!");
}
int width = maxX - minX;
int height = maxY - minY;
ByteBuffer data = BufferUtils.createByteBuffer(width * height * (image.getFormat().getBitsPerPixel() >> 3));
Image result = new Image(image.getFormat(), width, height, data, ColorSpace.sRGB);
PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat());
TexturePixel pixel = new TexturePixel();
for (int x = minX; x < maxX; ++x) {
for (int y = minY; y < maxY; ++y) {
pixelIO.read(image, 0, pixel, x, y);
pixelIO.write(result, 0, pixel, x - minX, y - minY);
}
}
return result;
}
/**
* This method applies the colorband and color factors to image type
* textures. If there is no colorband defined for the texture or the color
* factors are all equal to 1.0f then no changes are made.
*
* @param tex
* the texture structure
* @param image
* the image that will be altered if necessary
* @param blenderContext
* the blender context
*/
private void applyColorbandAndColorFactors(Structure tex, Image image, BlenderContext blenderContext) {
float rfac = ((Number) tex.getFieldValue("rfac")).floatValue();
float gfac = ((Number) tex.getFieldValue("gfac")).floatValue();
float bfac = ((Number) tex.getFieldValue("bfac")).floatValue();
float[][] colorBand = new ColorBand(tex, blenderContext).computeValues();
int depth = image.getDepth() == 0 ? 1 : image.getDepth();
if (colorBand != null) {
TexturePixel pixel = new TexturePixel();
PixelInputOutput imageIO = PixelIOFactory.getPixelIO(image.getFormat());
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) {
for (int x = 0; x < image.getWidth(); ++x) {
for (int y = 0; y < image.getHeight(); ++y) {
imageIO.read(image, layerIndex, pixel, x, y);
int colorbandIndex = (int) (pixel.alpha * 1000.0f);
pixel.red = colorBand[colorbandIndex][0] * rfac;
pixel.green = colorBand[colorbandIndex][1] * gfac;
pixel.blue = colorBand[colorbandIndex][2] * bfac;
pixel.alpha = colorBand[colorbandIndex][3];
imageIO.write(image, layerIndex, pixel, x, y);
}
}
}
} else if (rfac != 1.0f || gfac != 1.0f || bfac != 1.0f) {
TexturePixel pixel = new TexturePixel();
PixelInputOutput imageIO = PixelIOFactory.getPixelIO(image.getFormat());
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) {
for (int x = 0; x < image.getWidth(); ++x) {
for (int y = 0; y < image.getHeight(); ++y) {
imageIO.read(image, layerIndex, pixel, x, y);
pixel.red *= rfac;
pixel.green *= gfac;
pixel.blue *= bfac;
imageIO.write(image, layerIndex, pixel, x, y);
}
}
}
}
}
/**
* This method loads the texture from outside the blend file using the
* AssetManager that the blend file was loaded with. It returns a texture
* with a full assetKey that references the original texture so it later
* doesn't need to be packed when the model data is serialized. It searches
* the AssetManager for the full path if the model file is a relative path
* and will attempt to truncate the path if it is an absolute file path
* until the path can be found in the AssetManager. If the texture can not
* be found, it will issue a load attempt for the initial path anyway so the
* failed load can be reported by the AssetManagers callback methods for
* failed assets.
*
* @param name
* the path to the image
* @param imaflag
* the image flag
* @param blenderContext
* the blender context
* @return the loaded image or null if the image cannot be found
*/
protected Texture loadImageFromFile(String name, int imaflag, BlenderContext blenderContext) {
if (!name.contains(".")) {
return null; // no extension means not a valid image
}
// decide if the mipmaps will be generated
boolean generateMipmaps = false;
switch (blenderContext.getBlenderKey().getMipmapGenerationMethod()) {
case ALWAYS_GENERATE:
generateMipmaps = true;
break;
case NEVER_GENERATE:
break;
case GENERATE_WHEN_NEEDED:
generateMipmaps = (imaflag & 0x04) != 0;
break;
default:
throw new IllegalStateException("Unknown mipmap generation method: " + blenderContext.getBlenderKey().getMipmapGenerationMethod());
}
AssetManager assetManager = blenderContext.getAssetManager();
name = name.replace('\\', '/');
Texture result = null;
if (name.startsWith("//")) {
// This is a relative path, so try to find it relative to the .blend file
String relativePath = name.substring(2);
// Augument the path with blender key path
BlenderKey blenderKey = blenderContext.getBlenderKey();
int idx = blenderKey.getName().lastIndexOf('/');
String blenderAssetFolder = blenderKey.getName().substring(0, idx != -1 ? idx : 0);
String absoluteName = blenderAssetFolder + '/' + relativePath;
// Directly try to load texture so AssetManager can report missing textures
try {
TextureKey key = new TextureKey(absoluteName);
key.setFlipY(true);
key.setGenerateMips(generateMipmaps);
result = assetManager.loadTexture(key);
result.setKey(key);
} catch (AssetNotFoundException | AssetLoadException e) {
LOGGER.fine(e.getLocalizedMessage());
}
} else {
// This is a full path, try to truncate it until the file can be found
// this works as the assetManager root is most probably a part of the
// image path. E.g. AssetManager has a locator at c:/Files/ and the
// texture path is c:/Files/Textures/Models/Image.jpg.
// For this we create a list with every possible full path name from
// the asset name to the root. Image.jpg, Models/Image.jpg,
// Textures/Models/Image.jpg (bingo) etc.
List<String> assetNames = new ArrayList<String>();
String[] paths = name.split("\\/");
StringBuilder sb = new StringBuilder(paths[paths.length - 1]);// the asset name
assetNames.add(paths[paths.length - 1]);
for (int i = paths.length - 2; i >= 0; --i) {
sb.insert(0, '/');
sb.insert(0, paths[i]);
assetNames.add(0, sb.toString());
}
// Now try to locate the asset
for (String assetName : assetNames) {
try {
TextureKey key = new TextureKey(assetName);
key.setFlipY(true);
key.setGenerateMips(generateMipmaps);
AssetInfo info = assetManager.locateAsset(key);
if (info != null) {
Texture texture = assetManager.loadTexture(key);
result = texture;
// Set key explicitly here if other ways fail
texture.setKey(key);
// If texture is found return it;
return result;
}
} catch (AssetNotFoundException | AssetLoadException e) {
LOGGER.fine(e.getLocalizedMessage());
}
}
// The asset was not found in the loop above, call loadTexture with
// the original path once anyway so that the AssetManager can report
// the missing asset to subsystems.
try {
TextureKey key = new TextureKey(name);
assetManager.loadTexture(key);
} catch (AssetNotFoundException | AssetLoadException e) {
LOGGER.fine(e.getLocalizedMessage());
}
}
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")
public List<CombinedTexture> readTextureData(Structure structure, float[] diffuseColorArray, boolean skyTexture) throws BlenderFileException {
DynamicArray<Pointer> mtexsArray = (DynamicArray<Pointer>) structure.getFieldValue("mtex");
int separatedTextures = skyTexture ? 0 : ((Number) structure.getFieldValue("septex")).intValue();
List<TextureData> texturesList = new ArrayList<TextureData>();
for (int i = 0; i < mtexsArray.getTotalSize(); ++i) {
Pointer p = mtexsArray.get(i);
if (p.isNotNull() && (separatedTextures & 1 << i) == 0) {
TextureData textureData = new TextureData();
textureData.mtex = p.fetchData().get(0);
textureData.uvCoordinatesType = skyTexture ? UVCoordinatesType.TEXCO_ORCO.blenderValue : ((Number) textureData.mtex.getFieldValue("texco")).intValue();
textureData.projectionType = ((Number) textureData.mtex.getFieldValue("mapping")).intValue();
textureData.uvCoordinatesName = textureData.mtex.getFieldValue("uvName").toString();
if (textureData.uvCoordinatesName != null && textureData.uvCoordinatesName.trim().length() == 0) {
textureData.uvCoordinatesName = null;
}
Pointer pTex = (Pointer) textureData.mtex.getFieldValue("tex");
if (pTex.isNotNull()) {
Structure tex = pTex.fetchData().get(0);
textureData.textureStructure = tex;
texturesList.add(textureData);
}
}
}
LOGGER.info("Loading model's textures.");
List<CombinedTexture> loadedTextures = new ArrayList<CombinedTexture>();
if (blenderContext.getBlenderKey().isOptimiseTextures()) {
LOGGER.fine("Optimising the useage of model's textures.");
Map<Number, List<TextureData>> textureDataMap = this.sortTextures(texturesList);
for (Entry<Number, List<TextureData>> entry : textureDataMap.entrySet()) {
if (entry.getValue().size() > 0) {
CombinedTexture combinedTexture = new CombinedTexture(entry.getKey().intValue(), !skyTexture);
for (TextureData textureData : entry.getValue()) {
int texflag = ((Number) textureData.mtex.getFieldValue("texflag")).intValue();
boolean negateTexture = (texflag & 0x04) != 0;
Texture texture = this.getTexture(textureData.textureStructure, textureData.mtex, blenderContext);
if (texture != null) {
int blendType = ((Number) textureData.mtex.getFieldValue("blendtype")).intValue();
float[] color = new float[] { ((Number) textureData.mtex.getFieldValue("r")).floatValue(), ((Number) textureData.mtex.getFieldValue("g")).floatValue(), ((Number) textureData.mtex.getFieldValue("b")).floatValue() };
float colfac = ((Number) textureData.mtex.getFieldValue("colfac")).floatValue();
TextureBlender textureBlender = TextureBlenderFactory.createTextureBlender(texture.getImage().getFormat(), texflag, negateTexture, blendType, diffuseColorArray, color, colfac);
combinedTexture.add(texture, textureBlender, textureData.uvCoordinatesType, textureData.projectionType, textureData.textureStructure, textureData.uvCoordinatesName, blenderContext);
}
}
if (combinedTexture.getTexturesCount() > 0) {
loadedTextures.add(combinedTexture);
}
}
}
} else {
LOGGER.fine("No textures optimisation applied.");
int[] mappings = new int[] { MaterialContext.MTEX_COL, MaterialContext.MTEX_NOR, MaterialContext.MTEX_EMIT, MaterialContext.MTEX_SPEC, MaterialContext.MTEX_ALPHA, MaterialContext.MTEX_AMB };
for (TextureData textureData : texturesList) {
Texture texture = this.getTexture(textureData.textureStructure, textureData.mtex, blenderContext);
if (texture != null) {
Number mapto = (Number) textureData.mtex.getFieldValue("mapto");
int texflag = ((Number) textureData.mtex.getFieldValue("texflag")).intValue();
boolean negateTexture = (texflag & 0x04) != 0;
boolean colorSet = false;
for (int i = 0; i < mappings.length; ++i) {
if ((mappings[i] & mapto.intValue()) != 0) {
if(mappings[i] == MaterialContext.MTEX_COL) {
colorSet = true;
} else if(colorSet && mappings[i] == MaterialContext.MTEX_ALPHA) {
continue;
}
CombinedTexture combinedTexture = new CombinedTexture(mappings[i], !skyTexture);
int blendType = ((Number) textureData.mtex.getFieldValue("blendtype")).intValue();
float[] color = new float[] { ((Number) textureData.mtex.getFieldValue("r")).floatValue(), ((Number) textureData.mtex.getFieldValue("g")).floatValue(), ((Number) textureData.mtex.getFieldValue("b")).floatValue() };
float colfac = ((Number) textureData.mtex.getFieldValue("colfac")).floatValue();
TextureBlender textureBlender = TextureBlenderFactory.createTextureBlender(texture.getImage().getFormat(), texflag, negateTexture, blendType, diffuseColorArray, color, colfac);
combinedTexture.add(texture, textureBlender, textureData.uvCoordinatesType, textureData.projectionType, textureData.textureStructure, textureData.uvCoordinatesName, blenderContext);
if (combinedTexture.getTexturesCount() > 0) {// the added texture might not have been accepted (if for example loading generated textures is disabled)
loadedTextures.add(combinedTexture);
}
}
}
}
}
}
return loadedTextures;
}
/**
* This method sorts the textures by their mapping type. In each group only
* textures of one type are put (either two- or three-dimensional).
*
* @return a map with sorted textures
*/
private Map<Number, List<TextureData>> sortTextures(List<TextureData> textures) {
int[] mappings = new int[] { MaterialContext.MTEX_COL, MaterialContext.MTEX_NOR, MaterialContext.MTEX_EMIT, MaterialContext.MTEX_SPEC, MaterialContext.MTEX_ALPHA, MaterialContext.MTEX_AMB };
Map<Number, List<TextureData>> result = new HashMap<Number, List<TextureData>>();
for (TextureData data : textures) {
Number mapto = (Number) data.mtex.getFieldValue("mapto");
boolean colorSet = false;
for (int i = 0; i < mappings.length; ++i) {
if ((mappings[i] & mapto.intValue()) != 0) {
if(mappings[i] == MaterialContext.MTEX_COL) {
colorSet = true;
} else if(colorSet && mappings[i] == MaterialContext.MTEX_ALPHA) {
continue;
}
List<TextureData> datas = result.get(mappings[i]);
if (datas == null) {
datas = new ArrayList<TextureData>();
result.put(mappings[i], datas);
}
datas.add(data);
}
}
}
return result;
}
private static class TextureData {
public Structure mtex;
public Structure textureStructure;
public int uvCoordinatesType;
public int projectionType;
/** The name of the user's UV coordinates that are used for this texture. */
public String uvCoordinatesName;
}
}

@ -0,0 +1,392 @@
package com.jme3.scene.plugins.blender.textures;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
/**
* The class that stores the pixel values of a texture.
*
* @author Marcin Roguski (Kaelthas)
*/
public class TexturePixel implements Cloneable {
/** The pixel data. */
public float intensity, red, green, blue, alpha;
/**
* Copies the values from the given pixel.
*
* @param pixel
* the pixel that we read from
*/
public void fromPixel(TexturePixel pixel) {
this.intensity = pixel.intensity;
this.red = pixel.red;
this.green = pixel.green;
this.blue = pixel.blue;
this.alpha = pixel.alpha;
}
/**
* Copies the values from the given color.
*
* @param colorRGBA
* the color that we read from
*/
public void fromColor(ColorRGBA colorRGBA) {
this.red = colorRGBA.r;
this.green = colorRGBA.g;
this.blue = colorRGBA.b;
this.alpha = colorRGBA.a;
}
/**
* Copies the values from the given values.
*
* @param a
* the alpha value
* @param r
* the red value
* @param g
* the green value
* @param b
* the blue value
*/
public void fromARGB(float a, float r, float g, float b) {
this.alpha = a;
this.red = r;
this.green = g;
this.blue = b;
}
/**
* Copies the values from the given values.
*
* @param a
* the alpha value
* @param r
* the red value
* @param g
* the green value
* @param b
* the blue value
*/
public void fromARGB8(byte a, byte r, byte g, byte b) {
this.alpha = a >= 0 ? a / 255.0f : 1.0f - ~a / 255.0f;
this.red = r >= 0 ? r / 255.0f : 1.0f - ~r / 255.0f;
this.green = g >= 0 ? g / 255.0f : 1.0f - ~g / 255.0f;
this.blue = b >= 0 ? b / 255.0f : 1.0f - ~b / 255.0f;
}
/**
* Copies the values from the given values.
*
* @param a
* the alpha value
* @param r
* the red value
* @param g
* the green value
* @param b
* the blue value
*/
public void fromARGB16(short a, short r, short g, short b) {
this.alpha = a >= 0 ? a / 65535.0f : 1.0f - ~a / 65535.0f;
this.red = r >= 0 ? r / 65535.0f : 1.0f - ~r / 65535.0f;
this.green = g >= 0 ? g / 65535.0f : 1.0f - ~g / 65535.0f;
this.blue = b >= 0 ? b / 65535.0f : 1.0f - ~b / 65535.0f;
}
/**
* Copies the intensity from the given value.
*
* @param intensity
* the intensity value
*/
public void fromIntensity(byte intensity) {
this.intensity = intensity >= 0 ? intensity / 255.0f : 1.0f - ~intensity / 255.0f;
}
/**
* Copies the intensity from the given value.
*
* @param intensity
* the intensity value
*/
public void fromIntensity(short intensity) {
this.intensity = intensity >= 0 ? intensity / 65535.0f : 1.0f - ~intensity / 65535.0f;
}
/**
* This method sets the alpha value (converts it to float number from range
* [0, 1]).
*
* @param alpha
* the alpha value
*/
public void setAlpha(byte alpha) {
this.alpha = alpha >= 0 ? alpha / 255.0f : 1.0f - ~alpha / 255.0f;
}
/**
* This method sets the alpha value (converts it to float number from range
* [0, 1]).
*
* @param alpha
* the alpha value
*/
public void setAlpha(short alpha) {
this.alpha = alpha >= 0 ? alpha / 65535.0f : 1.0f - ~alpha / 65535.0f;
}
/**
* Copies the values from the given integer that stores the ARGB8 data.
*
* @param argb8
* the data stored in an integer
*/
public void fromARGB8(int argb8) {
byte pixelValue = (byte) ((argb8 & 0xFF000000) >> 24);
this.alpha = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f;
pixelValue = (byte) ((argb8 & 0xFF0000) >> 16);
this.red = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f;
pixelValue = (byte) ((argb8 & 0xFF00) >> 8);
this.green = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f;
pixelValue = (byte) (argb8 & 0xFF);
this.blue = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f;
}
/**
* Stores RGBA values in the given array.
*
* @param result
* the array to store values
*/
public void toRGBA(float[] result) {
result[0] = this.red;
result[1] = this.green;
result[2] = this.blue;
result[3] = this.alpha;
}
/**
* Stores the data in the given table.
*
* @param result
* the result table
*/
public void toRGBA8(byte[] result) {
result[0] = (byte) (this.red * 255.0f);
result[1] = (byte) (this.green * 255.0f);
result[2] = (byte) (this.blue * 255.0f);
result[3] = (byte) (this.alpha * 255.0f);
}
/**
* Stores the pixel values in the integer.
*
* @return the integer that stores the pixel values
*/
public int toARGB8() {
int result = 0;
int b = (int) (this.alpha * 255.0f);
result |= b << 24;
b = (int) (this.red * 255.0f);
result |= b << 16;
b = (int) (this.green * 255.0f);
result |= b << 8;
b = (int) (this.blue * 255.0f);
result |= b;
return result;
}
/**
* @return the intensity of the pixel
*/
public byte getInt() {
return (byte) (this.intensity * 255.0f);
}
/**
* @return the alpha value of the pixel
*/
public byte getA8() {
return (byte) (this.alpha * 255.0f);
}
/**
* @return the alpha red of the pixel
*/
public byte getR8() {
return (byte) (this.red * 255.0f);
}
/**
* @return the green value of the pixel
*/
public byte getG8() {
return (byte) (this.green * 255.0f);
}
/**
* @return the blue value of the pixel
*/
public byte getB8() {
return (byte) (this.blue * 255.0f);
}
/**
* @return the alpha value of the pixel
*/
public short getA16() {
return (byte) (this.alpha * 65535.0f);
}
/**
* @return the alpha red of the pixel
*/
public short getR16() {
return (byte) (this.red * 65535.0f);
}
/**
* @return the green value of the pixel
*/
public short getG16() {
return (byte) (this.green * 65535.0f);
}
/**
* @return the blue value of the pixel
*/
public short getB16() {
return (byte) (this.blue * 65535.0f);
}
/**
* Merges two pixels (adds the values of each color).
*
* @param pixel
* the pixel we merge with
*/
public void merge(TexturePixel pixel) {
float oneMinusAlpha = 1 - pixel.alpha;
this.red = oneMinusAlpha * this.red + pixel.alpha * pixel.red;
this.green = oneMinusAlpha * this.green + pixel.alpha * pixel.green;
this.blue = oneMinusAlpha * this.blue + pixel.alpha * pixel.blue;
this.alpha = (this.alpha + pixel.alpha) * 0.5f;
}
/**
* Mixes two pixels.
*
* @param pixel
* the pixel we mix with
*/
public void mix(TexturePixel pixel) {
this.red = 0.5f * (this.red + pixel.red);
this.green = 0.5f * (this.green + pixel.green);
this.blue = 0.5f * (this.blue + pixel.blue);
this.alpha = 0.5f * (this.alpha + pixel.alpha);
this.intensity = 0.5f * (this.intensity + pixel.intensity);
}
/**
* This method negates the colors.
*/
public void negate() {
this.red = 1.0f - this.red;
this.green = 1.0f - this.green;
this.blue = 1.0f - this.blue;
this.alpha = 1.0f - this.alpha;
}
/**
* This method clears the pixel values.
*/
public void clear() {
this.intensity = this.blue = this.red = this.green = this.alpha = 0.0f;
}
/**
* This method adds the calues of the given pixel to the current pixel.
*
* @param pixel
* the pixel we add
*/
public void add(TexturePixel pixel) {
this.red += pixel.red;
this.green += pixel.green;
this.blue += pixel.blue;
this.alpha += pixel.alpha;
this.intensity += pixel.intensity;
}
/**
* This method adds the calues of the given pixel to the current pixel.
*
* @param pixel
* the pixel we add
*/
public void add(ColorRGBA pixel) {
this.red += pixel.r;
this.green += pixel.g;
this.blue += pixel.b;
this.alpha += pixel.a;
}
/**
* This method multiplies the values of the given pixel by the given value.
*
* @param value
* multiplication factor
*/
public void mult(float value) {
this.red *= value;
this.green *= value;
this.blue *= value;
this.alpha *= value;
this.intensity *= value;
}
/**
* This method divides the values of the given pixel by the given value.
* ATTENTION! Beware of the zero value. This will cause you NaN's in the
* pixel values.
*
* @param value
* division factor
*/
public void divide(float value) {
this.red /= value;
this.green /= value;
this.blue /= value;
this.alpha /= value;
this.intensity /= value;
}
/**
* This method clamps the pixel values to the given borders.
*
* @param min
* the minimum value
* @param max
* the maximum value
*/
public void clamp(float min, float max) {
this.red = FastMath.clamp(this.red, min, max);
this.green = FastMath.clamp(this.green, min, max);
this.blue = FastMath.clamp(this.blue, min, max);
this.alpha = FastMath.clamp(this.alpha, min, max);
this.intensity = FastMath.clamp(this.intensity, min, max);
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "[" + red + ", " + green + ", " + blue + ", " + alpha + " {" + intensity + "}]";
}
}

@ -0,0 +1,662 @@
package com.jme3.scene.plugins.blender.textures;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import jme3tools.converters.ImageToAwt;
import com.jme3.bounding.BoundingBox;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender;
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory;
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
/**
* This texture holds a set of images for each face in the specified mesh. It
* helps to flatten 3D texture, merge 3D and 2D textures and merge 2D textures
* with different UV coordinates.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class TriangulatedTexture extends Texture2D {
/** The result image format. */
private Format format;
/** The collection of images for each face. */
private Collection<TriangleTextureElement> faceTextures;
/**
* The maximum texture size (width/height). This is taken from the blender
* key.
*/
private int maxTextureSize;
/** A variable that can prevent removing identical textures. */
private boolean keepIdenticalTextures = false;
/** The result texture. */
private Texture2D resultTexture;
/** The result texture's UV coordinates. */
private List<Vector2f> resultUVS;
/**
* This method triangulates the given flat texture. The given texture is not
* changed.
*
* @param texture2d
* the texture to be triangulated
* @param uvs
* the UV coordinates for each face
*/
public TriangulatedTexture(Texture2D texture2d, List<Vector2f> uvs, BlenderContext blenderContext) {
maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize();
faceTextures = new TreeSet<TriangleTextureElement>(new Comparator<TriangleTextureElement>() {
@Override
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
return o1.faceIndex - o2.faceIndex;
}
});
int facesCount = uvs.size() / 3;
for (int i = 0; i < facesCount; ++i) {
faceTextures.add(new TriangleTextureElement(i, texture2d.getImage(), uvs, true, blenderContext));
}
format = texture2d.getImage().getFormat();
}
/**
* Constructor that simply stores precalculated images.
*
* @param faceTextures
* a collection of images for the mesh's faces
* @param blenderContext
* the blender context
*/
public TriangulatedTexture(Collection<TriangleTextureElement> faceTextures, BlenderContext blenderContext) {
maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize();
this.faceTextures = faceTextures;
for (TriangleTextureElement faceTextureElement : faceTextures) {
if (format == null) {
format = faceTextureElement.image.getFormat();
} else if (format != faceTextureElement.image.getFormat()) {
throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
}
}
}
/**
* This method blends the each image using the given blender and taking base
* texture into consideration.
*
* @param textureBlender
* the texture blender that holds the blending definition
* @param baseTexture
* the texture that is 'below' the current texture (can be null)
* @param blenderContext
* the blender context
*/
public void blend(TextureBlender textureBlender, TriangulatedTexture baseTexture, BlenderContext blenderContext) {
Format newFormat = null;
for (TriangleTextureElement triangleTextureElement : faceTextures) {
Image baseImage = baseTexture == null ? null : baseTexture.getFaceTextureElement(triangleTextureElement.faceIndex).image;
triangleTextureElement.image = textureBlender.blend(triangleTextureElement.image, baseImage, blenderContext);
if (newFormat == null) {
newFormat = triangleTextureElement.image.getFormat();
} else if (newFormat != triangleTextureElement.image.getFormat()) {
throw new IllegalArgumentException("Face texture element images MUST have the same image format!");
}
}
format = newFormat;
}
/**
* This method alters the images to fit them into UV coordinates of the
* given target texture.
*
* @param targetTexture
* the texture to whose UV coordinates we fit current images
* @param blenderContext
* the blender context
*/
public void castToUVS(TriangulatedTexture targetTexture, BlenderContext blenderContext) {
int[] sourceSize = new int[2], targetSize = new int[2];
ImageLoader imageLoader = new ImageLoader();
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
for (TriangleTextureElement entry : faceTextures) {
TriangleTextureElement targetFaceTextureElement = targetTexture.getFaceTextureElement(entry.faceIndex);
Vector2f[] dest = targetFaceTextureElement.uv;
// get the sizes of the source and target images
sourceSize[0] = entry.image.getWidth();
sourceSize[1] = entry.image.getHeight();
targetSize[0] = targetFaceTextureElement.image.getWidth();
targetSize[1] = targetFaceTextureElement.image.getHeight();
// create triangle transformation
AffineTransform affineTransform = textureHelper.createAffineTransform(entry.uv, dest, sourceSize, targetSize);
// compute the result texture
BufferedImage sourceImage = ImageToAwt.convert(entry.image, false, true, 0);
BufferedImage targetImage = new BufferedImage(targetSize[0], targetSize[1], sourceImage.getType());
Graphics2D g = targetImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(sourceImage, affineTransform, null);
g.dispose();
Image output = imageLoader.load(targetImage, false);
entry.image = output;
entry.uv[0].set(dest[0]);
entry.uv[1].set(dest[1]);
entry.uv[2].set(dest[2]);
}
}
/**
* This method returns the flat texture. It is calculated if required or if
* it was not created before. Images that are identical are discarded to
* reduce the texture size.
*
* @param rebuild
* a variable that forces texture recomputation (even if it was
* computed vefore)
* @return flat result texture (all images merged into one)
*/
public Texture2D getResultTexture(boolean rebuild) {
if (resultTexture == null || rebuild) {
// sorting the parts by their height (from highest to the lowest)
List<TriangleTextureElement> list = new ArrayList<TriangleTextureElement>(faceTextures);
Collections.sort(list, new Comparator<TriangleTextureElement>() {
@Override
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) {
return o2.image.getHeight() - o1.image.getHeight();
}
});
// arraging the images on the resulting image (calculating the result image width and height)
Set<Integer> duplicatedFaceIndexes = new HashSet<Integer>();
int resultImageHeight = list.get(0).image.getHeight();
int resultImageWidth = 0;
int currentXPos = 0, currentYPos = 0;
Map<TriangleTextureElement, Integer[]> imageLayoutData = new HashMap<TriangleTextureElement, Integer[]>(list.size());
while (list.size() > 0) {
TriangleTextureElement currentElement = list.remove(0);
if (currentXPos + currentElement.image.getWidth() > maxTextureSize) {
currentXPos = 0;
currentYPos = resultImageHeight;
resultImageHeight += currentElement.image.getHeight();
}
Integer[] currentPositions = new Integer[] { currentXPos, currentYPos };
imageLayoutData.put(currentElement, currentPositions);
if (keepIdenticalTextures) {// removing identical images
for (int i = 0; i < list.size(); ++i) {
if (currentElement.image.equals(list.get(i).image)) {
duplicatedFaceIndexes.add(list.get(i).faceIndex);
imageLayoutData.put(list.remove(i--), currentPositions);
}
}
}
currentXPos += currentElement.image.getWidth();
resultImageWidth = Math.max(resultImageWidth, currentXPos);
// currentYPos += currentElement.image.getHeight();
// TODO: implement that to compact the result image
// try to add smaller images below the current one
// int remainingHeight = resultImageHeight -
// currentElement.image.getHeight();
// while(remainingHeight > 0) {
// for(int i=list.size() - 1;i>=0;--i) {
//
// }
// }
}
// computing the result UV coordinates
resultUVS = new ArrayList<Vector2f>(imageLayoutData.size() * 3);
for (int i = 0; i < imageLayoutData.size() * 3; ++i) {
resultUVS.add(null);
}
Vector2f[] uvs = new Vector2f[3];
for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) {
Integer[] position = entry.getValue();
entry.getKey().computeFinalUVCoordinates(resultImageWidth, resultImageHeight, position[0], position[1], uvs);
resultUVS.set(entry.getKey().faceIndex * 3, uvs[0]);
resultUVS.set(entry.getKey().faceIndex * 3 + 1, uvs[1]);
resultUVS.set(entry.getKey().faceIndex * 3 + 2, uvs[2]);
}
Image resultImage = new Image(format, resultImageWidth, resultImageHeight, BufferUtils.createByteBuffer(resultImageWidth * resultImageHeight * (format.getBitsPerPixel() >> 3)), ColorSpace.Linear);
resultTexture = new Texture2D(resultImage);
for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) {
if (!duplicatedFaceIndexes.contains(entry.getKey().faceIndex)) {
this.draw(resultImage, entry.getKey().image, entry.getValue()[0], entry.getValue()[1]);
}
}
// setting additional data
resultTexture.setWrap(WrapAxis.S, this.getWrap(WrapAxis.S));
resultTexture.setWrap(WrapAxis.T, this.getWrap(WrapAxis.T));
resultTexture.setMagFilter(this.getMagFilter());
resultTexture.setMinFilter(this.getMinFilter());
}
return resultTexture;
}
/**
* @return the result flat texture
*/
public Texture2D getResultTexture() {
return this.getResultTexture(false);
}
/**
* @return the result texture's UV coordinates
*/
public List<Vector2f> getResultUVS() {
this.getResultTexture();// this is called here to make sure that the result UVS are computed
return resultUVS;
}
/**
* This method returns a single image element for the given face index.
*
* @param faceIndex
* the face index
* @return image element for the required face index
* @throws IllegalStateException
* this exception is thrown if the current image set does not
* contain an image for the given face index
*/
public TriangleTextureElement getFaceTextureElement(int faceIndex) {
for (TriangleTextureElement textureElement : faceTextures) {
if (textureElement.faceIndex == faceIndex) {
return textureElement;
}
}
throw new IllegalStateException("No face texture element found for index: " + faceIndex);
}
/**
* @return the amount of texture faces
*/
public int getFaceTextureCount() {
return faceTextures.size();
}
/**
* Tells the object wheather to keep or reduce identical face textures.
*
* @param keepIdenticalTextures
* keeps or discards identical textures
*/
public void setKeepIdenticalTextures(boolean keepIdenticalTextures) {
this.keepIdenticalTextures = keepIdenticalTextures;
}
/**
* This method draws the source image on the target image starting with the
* specified positions.
*
* @param target
* the target image
* @param source
* the source image
* @param targetXPos
* start X position on the target image
* @param targetYPos
* start Y position on the target image
*/
private void draw(Image target, Image source, int targetXPos, int targetYPos) {
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(source.getFormat());
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(target.getFormat());
TexturePixel pixel = new TexturePixel();
for (int x = 0; x < source.getWidth(); ++x) {
for (int y = 0; y < source.getHeight(); ++y) {
sourceIO.read(source, 0, pixel, x, y);
targetIO.write(target, 0, pixel, targetXPos + x, targetYPos + y);
}
}
}
/**
* A class that represents an image for a single face of the mesh.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */static class TriangleTextureElement {
/** The image for the face. */
public Image image;
/** The UV coordinates for the image. */
public final Vector2f[] uv;
/** The index of the face this image refers to. */
public final int faceIndex;
/**
* Constructor that creates the image element from the given texture and
* UV coordinates (it cuts out the smallest rectasngle possible from the
* given image that will hold the triangle defined by the given UV
* coordinates). After the image is cut out the UV coordinates are
* recalculated to be fit for the new image.
*
* @param faceIndex
* the index of mesh's face this image refers to
* @param sourceImage
* the source image
* @param uvCoordinates
* the UV coordinates that define the image
*/
public TriangleTextureElement(int faceIndex, Image sourceImage, List<Vector2f> uvCoordinates, boolean wholeUVList, BlenderContext blenderContext) {
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
this.faceIndex = faceIndex;
uv = wholeUVList ? new Vector2f[] { uvCoordinates.get(faceIndex * 3).clone(), uvCoordinates.get(faceIndex * 3 + 1).clone(), uvCoordinates.get(faceIndex * 3 + 2).clone() } : new Vector2f[] { uvCoordinates.get(0).clone(), uvCoordinates.get(1).clone(), uvCoordinates.get(2).clone() };
// be careful here, floating point operations might cause the
// texture positions to be inapropriate
int[][] texturePosition = new int[3][2];
for (int i = 0; i < texturePosition.length; ++i) {
texturePosition[i][0] = textureHelper.getPixelPosition(uv[i].x, sourceImage.getWidth());
texturePosition[i][1] = textureHelper.getPixelPosition(uv[i].y, sourceImage.getHeight());
}
// calculating the extent of the texture
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
float minUVX = Float.MAX_VALUE, minUVY = Float.MAX_VALUE;
float maxUVX = Float.MIN_VALUE, maxUVY = Float.MIN_VALUE;
for (int i = 0; i < texturePosition.length; ++i) {
minX = Math.min(texturePosition[i][0], minX);
minY = Math.min(texturePosition[i][1], minY);
maxX = Math.max(texturePosition[i][0], maxX);
maxY = Math.max(texturePosition[i][1], maxY);
minUVX = Math.min(uv[i].x, minUVX);
minUVY = Math.min(uv[i].y, minUVY);
maxUVX = Math.max(uv[i].x, maxUVX);
maxUVY = Math.max(uv[i].y, maxUVY);
}
int width = maxX - minX;
int height = maxY - minY;
if (width == 0) {
width = 1;
}
if (height == 0) {
height = 1;
}
// copy the pixel from the texture to the result image
PixelInputOutput pixelReader = PixelIOFactory.getPixelIO(sourceImage.getFormat());
TexturePixel pixel = new TexturePixel();
ByteBuffer data = BufferUtils.createByteBuffer(width * height * 4);
for (int y = minY; y < maxY; ++y) {
for (int x = minX; x < maxX; ++x) {
int xPos = x >= sourceImage.getWidth() ? x - sourceImage.getWidth() : x;
int yPos = y >= sourceImage.getHeight() ? y - sourceImage.getHeight() : y;
pixelReader.read(sourceImage, 0, pixel, xPos, yPos);
data.put(pixel.getR8());
data.put(pixel.getG8());
data.put(pixel.getB8());
data.put(pixel.getA8());
}
}
image = new Image(Format.RGBA8, width, height, data, ColorSpace.Linear);
// modify the UV values so that they fit the new image
float heightUV = maxUVY - minUVY;
float widthUV = maxUVX - minUVX;
for (int i = 0; i < uv.length; ++i) {
// first translate it to the image borders
uv[i].x -= minUVX;
uv[i].y -= minUVY;
// then scale so that it fills the whole area
uv[i].x /= widthUV;
uv[i].y /= heightUV;
}
}
/**
* Constructor that creates an image element from the 3D texture
* (generated texture). It computes a flat smallest rectangle that can
* hold a (3D) triangle defined by the given UV coordinates. Then it
* defines the image pixels for points in 3D space that define the
* calculated rectangle.
*
* @param faceIndex
* the face index this image refers to
* @param boundingBox
* the bounding box of the mesh
* @param texture
* the texture that allows to compute a pixel value in 3D
* space
* @param uv
* the UV coordinates of the mesh
* @param blenderContext
* the blender context
*/
public TriangleTextureElement(int faceIndex, BoundingBox boundingBox, GeneratedTexture texture, Vector3f[] uv, int[] uvIndices, BlenderContext blenderContext) {
this.faceIndex = faceIndex;
// compute the face vertices from the UV coordinates
float width = boundingBox.getXExtent() * 2;
float height = boundingBox.getYExtent() * 2;
float depth = boundingBox.getZExtent() * 2;
Vector3f min = boundingBox.getMin(null);
Vector3f v1 = min.add(uv[uvIndices[0]].x * width, uv[uvIndices[0]].y * height, uv[uvIndices[0]].z * depth);
Vector3f v2 = min.add(uv[uvIndices[1]].x * width, uv[uvIndices[1]].y * height, uv[uvIndices[1]].z * depth);
Vector3f v3 = min.add(uv[uvIndices[2]].x * width, uv[uvIndices[2]].y * height, uv[uvIndices[2]].z * depth);
// get the rectangle envelope for the triangle
RectangleEnvelope envelope = this.getTriangleEnvelope(v1, v2, v3);
// create the result image
Format imageFormat = texture.getImage().getFormat();
int imageWidth = (int) (envelope.width * blenderContext.getBlenderKey().getGeneratedTexturePPU());
if (imageWidth == 0) {
imageWidth = 1;
}
int imageHeight = (int) (envelope.height * blenderContext.getBlenderKey().getGeneratedTexturePPU());
if (imageHeight == 0) {
imageHeight = 1;
}
ByteBuffer data = BufferUtils.createByteBuffer(imageWidth * imageHeight * (imageFormat.getBitsPerPixel() >> 3));
image = new Image(texture.getImage().getFormat(), imageWidth, imageHeight, data, ColorSpace.Linear);
// computing the pixels
PixelInputOutput pixelWriter = PixelIOFactory.getPixelIO(imageFormat);
TexturePixel pixel = new TexturePixel();
float[] uvs = new float[3];
Vector3f point = new Vector3f(envelope.min);
Vector3f vecY = new Vector3f();
Vector3f wDelta = new Vector3f(envelope.w).multLocal(1.0f / imageWidth);
Vector3f hDelta = new Vector3f(envelope.h).multLocal(1.0f / imageHeight);
for (int x = 0; x < imageWidth; ++x) {
for (int y = 0; y < imageHeight; ++y) {
this.toTextureUV(boundingBox, point, uvs);
texture.getPixel(pixel, uvs[0], uvs[1], uvs[2]);
pixelWriter.write(image, 0, pixel, x, y);
point.addLocal(hDelta);
}
vecY.addLocal(wDelta);
point.set(envelope.min).addLocal(vecY);
}
// preparing UV coordinates for the flatted texture
this.uv = new Vector2f[3];
this.uv[0] = new Vector2f(FastMath.clamp(v1.subtract(envelope.min).length(), 0, Float.MAX_VALUE) / envelope.height, 0);
Vector3f heightDropPoint = v2.subtract(envelope.w);// w is directed from the base to v2
this.uv[1] = new Vector2f(1, heightDropPoint.subtractLocal(envelope.min).length() / envelope.height);
this.uv[2] = new Vector2f(0, 1);
}
/**
* This method computes the final UV coordinates for the image (after it
* is combined with other images and drawed on the result image).
*
* @param totalImageWidth
* the result image width
* @param totalImageHeight
* the result image height
* @param xPos
* the most left x coordinate of the image
* @param yPos
* the most top y coordinate of the image
* @param result
* a vector where the result is stored
*/
public void computeFinalUVCoordinates(int totalImageWidth, int totalImageHeight, int xPos, int yPos, Vector2f[] result) {
for (int i = 0; i < 3; ++i) {
result[i] = new Vector2f();
result[i].x = xPos / (float) totalImageWidth + uv[i].x * (image.getWidth() / (float) totalImageWidth);
result[i].y = yPos / (float) totalImageHeight + uv[i].y * (image.getHeight() / (float) totalImageHeight);
}
}
/**
* This method converts the given point into 3D UV coordinates.
*
* @param boundingBox
* the bounding box of the mesh
* @param point
* the point to be transformed
* @param uvs
* the result UV coordinates
*/
private void toTextureUV(BoundingBox boundingBox, Vector3f point, float[] uvs) {
uvs[0] = (point.x - boundingBox.getCenter().x) / (boundingBox.getXExtent() == 0 ? 1 : boundingBox.getXExtent());
uvs[1] = (point.y - boundingBox.getCenter().y) / (boundingBox.getYExtent() == 0 ? 1 : boundingBox.getYExtent());
uvs[2] = (point.z - boundingBox.getCenter().z) / (boundingBox.getZExtent() == 0 ? 1 : boundingBox.getZExtent());
// UVS cannot go outside <0, 1> range, but since we are generating texture for triangle envelope it might happen that
// some points of the envelope will exceet the bounding box of the mesh thus generating uvs outside the range
for (int i = 0; i < 3; ++i) {
uvs[i] = FastMath.clamp(uvs[i], 0, 1);
}
}
/**
* This method returns an envelope of a minimal rectangle, that is set
* in 3D space, and contains the given triangle.
*
* @param triangle
* the triangle
* @return a rectangle minimum and maximum point and height and width
*/
private RectangleEnvelope getTriangleEnvelope(Vector3f v1, Vector3f v2, Vector3f v3) {
Vector3f h = v3.subtract(v1);// the height of the resulting rectangle
Vector3f temp = v2.subtract(v1);
float field = 0.5f * h.cross(temp).length();// the field of the rectangle: Field = 0.5 * ||h x temp||
if (field <= 0.0f) {
return new RectangleEnvelope(v1);// return single point envelope
}
float cosAlpha = h.dot(temp) / (h.length() * temp.length());// the cosinus of angle betweenh and temp
float triangleHeight = 2 * field / h.length();// the base of the height is the h vector
// now calculate the distance between v1 vertex and the point where
// the above calculated height 'touches' the base line (it can be
// settled outside the h vector)
float x = Math.abs((float) Math.sqrt(FastMath.clamp(temp.lengthSquared() - triangleHeight * triangleHeight, 0, Float.MAX_VALUE))) * Math.signum(cosAlpha);
// now get the height base point
Vector3f xPoint = v1.add(h.normalize().multLocal(x));
// get the minimum point of the envelope
Vector3f min = x < 0 ? xPoint : v1;
if (x < 0) {
h = v3.subtract(min);
} else if (x > h.length()) {
h = xPoint.subtract(min);
}
Vector3f envelopeWidth = v2.subtract(xPoint);
return new RectangleEnvelope(min, envelopeWidth, h);
}
}
/**
* A class that represents a flat rectangle in 3D space that is built on a
* triangle in 3D space.
*
* @author Marcin Roguski (Kaelthas)
*/
private static class RectangleEnvelope {
/** The minimum point of the rectangle. */
public final Vector3f min;
/** The width vector. */
public final Vector3f w;
/** The height vector. */
public final Vector3f h;
/** The width of the rectangle. */
public final float width;
/** The height of the rectangle. */
public final float height;
/**
* Constructs a rectangle that actually holds a point, not a triangle.
* This is a special case that is sometimes used when generating a
* texture where UV coordinates are defined by normals instead of
* vertices.
*
* @param pointPosition
* a position in 3D space
*/
public RectangleEnvelope(Vector3f pointPosition) {
min = pointPosition;
h = w = Vector3f.ZERO;
width = height = 1;
}
/**
* Constructs a rectangle envelope.
*
* @param min
* the minimum rectangle point
* @param w
* the width vector
* @param h
* the height vector
*/
public RectangleEnvelope(Vector3f min, Vector3f w, Vector3f h) {
this.min = min;
this.h = h;
this.w = w;
width = w.length();
height = h.length();
}
@Override
public String toString() {
return "Envelope[min = " + min + ", w = " + w + ", h = " + h + "]";
}
}
@Override
public Texture createSimpleClone() {
return null;
}
}

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

Loading…
Cancel
Save