Removing the jme3-blender subproject and related references since it now lives in its

own repository: https://github.com/jMonkeyEngine-Contributions/BlenderLoader
master
pspeed42 5 years ago
parent 94451c4d35
commit 221acaadfe
  1. 2
      README.md
  2. 12
      jme3-blender/build.gradle
  3. 83
      jme3-blender/examples/src/main/java/jme3test/blender/TestBlenderLoader.java
  4. 136
      jme3-blender/examples/src/main/java/jme3test/light/TestTangentGenBadModels.java
  5. 95
      jme3-blender/examples/src/main/java/jme3test/model/anim/TestBlenderAnim.java
  6. 94
      jme3-blender/examples/src/main/java/jme3test/model/anim/TestBlenderObjectAnim.java
  7. 733
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  8. 69
      jme3-blender/src/main/java/com/jme3/asset/GeneratedTextureKey.java
  9. 193
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java
  10. 767
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  11. 419
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  12. 40
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderModelLoader.java
  13. 391
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java
  14. 134
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java
  15. 400
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneContext.java
  16. 133
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BoneEnvelope.java
  17. 317
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java
  18. 148
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/cameras/CameraHelper.java
  19. 88
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/BoneConstraint.java
  20. 186
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java
  21. 476
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java
  22. 397
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SimulationNode.java
  23. 41
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SkeletonConstraint.java
  24. 32
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/SpatialConstraint.java
  25. 165
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/VirtualTrack.java
  26. 162
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinition.java
  27. 84
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionDistLimit.java
  28. 126
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionFactory.java
  29. 236
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionIK.java
  30. 107
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLike.java
  31. 106
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionLocLimit.java
  32. 61
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionMaintainVolume.java
  33. 34
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionNull.java
  34. 87
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLike.java
  35. 129
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionRotLimit.java
  36. 74
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLike.java
  37. 96
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionSizeLimit.java
  38. 81
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/ConstraintDefinitionTransLike.java
  39. 40
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/definitions/UnsupportedConstraintDefinition.java
  40. 172
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/BezierCurve.java
  41. 159
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesHelper.java
  42. 890
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/curves/CurvesTemporalMesh.java
  43. 77
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java
  44. 371
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java
  45. 203
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DnaBlockData.java
  46. 135
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/DynamicArray.java
  47. 327
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java
  48. 213
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/FileBlockHeader.java
  49. 189
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Pointer.java
  50. 328
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Structure.java
  51. 214
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java
  52. 116
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/lights/LightHelper.java
  53. 26
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/IAlphaMask.java
  54. 366
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  55. 387
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialHelper.java
  56. 583
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/DQuaternion.java
  57. 190
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/DTransform.java
  58. 210
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Matrix.java
  59. 869
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Vector3d.java
  60. 349
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java
  61. 613
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java
  62. 305
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/IndexesLoop.java
  63. 364
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshBuffers.java
  64. 381
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  65. 93
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Point.java
  66. 767
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java
  67. 113
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java
  68. 241
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArrayModifier.java
  69. 200
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/MaskModifier.java
  70. 196
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/MirrorModifier.java
  71. 92
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/Modifier.java
  72. 117
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ModifierHelper.java
  73. 107
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ParticlesModifier.java
  74. 642
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/SubdivisionSurfaceModifier.java
  75. 54
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/TriangulateModifier.java
  76. 520
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java
  77. 365
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/Properties.java
  78. 192
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/particles/ParticlesHelper.java
  79. 401
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ColorBand.java
  80. 543
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/CombinedTexture.java
  81. 157
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/DDSTexelData.java
  82. 282
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/GeneratedTexture.java
  83. 136
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageLoader.java
  84. 473
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/ImageUtils.java
  85. 691
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TextureHelper.java
  86. 392
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TexturePixel.java
  87. 662
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/TriangulatedTexture.java
  88. 420
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/UVCoordinatesGenerator.java
  89. 240
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/UVProjectionGenerator.java
  90. 86
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/UserUVCollection.java
  91. 139
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/AbstractTextureBlender.java
  92. 53
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlender.java
  93. 275
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderAWT.java
  94. 131
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderDDS.java
  95. 117
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderFactory.java
  96. 259
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/blending/TextureBlenderLuminance.java
  97. 814
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/generating/NoiseGenerator.java
  98. 128
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/generating/TextureGenerator.java
  99. 138
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/generating/TextureGeneratorBlend.java
  100. 121
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/generating/TextureGeneratorClouds.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -37,7 +37,7 @@ Note: The master branch on GitHub is a development version of the engine and is
- NetBeans Platform
- Gradle
Plus a bunch of awesome libraries & tight integrations like Bullet, Blender, NiftyGUI and other goodies.
Plus a bunch of awesome libraries & tight integrations like Bullet, NiftyGUI and other goodies.
### Documentation

@ -1,12 +0,0 @@
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')
}

@ -1,83 +0,0 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package jme3test.blender;
import com.jme3.app.SimpleApplication;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
public class TestBlenderLoader extends SimpleApplication {
public static void main(String[] args){
TestBlenderLoader app = new TestBlenderLoader();
app.start();
}
@Override
public void simpleInitApp() {
viewPort.setBackgroundColor(ColorRGBA.DarkGray);
//load model with packed images
Spatial ogre = assetManager.loadModel("Blender/2.4x/Sinbad.blend");
rootNode.attachChild(ogre);
//load model with referenced images
Spatial track = assetManager.loadModel("Blender/2.4x/MountainValley_Track.blend");
rootNode.attachChild(track);
// sunset light
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(-0.1f,-0.7f,1).normalizeLocal());
dl.setColor(new ColorRGBA(0.44f, 0.30f, 0.20f, 1.0f));
rootNode.addLight(dl);
// skylight
dl = new DirectionalLight();
dl.setDirection(new Vector3f(-0.6f,-1,-0.6f).normalizeLocal());
dl.setColor(new ColorRGBA(0.10f, 0.22f, 0.44f, 1.0f));
rootNode.addLight(dl);
// white ambient light
dl = new DirectionalLight();
dl.setDirection(new Vector3f(1, -0.5f,-0.1f).normalizeLocal());
dl.setColor(new ColorRGBA(0.80f, 0.70f, 0.80f, 1.0f));
rootNode.addLight(dl);
}
@Override
public void simpleUpdate(float tpf){
}
}

@ -1,136 +0,0 @@
package jme3test.light;
import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.*;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.TangentBinormalGenerator;
/**
*
* @author Kirusha
*/
public class TestTangentGenBadModels extends SimpleApplication {
float angle;
PointLight pl;
Geometry lightMdl;
public static void main(String[] args){
TestTangentGenBadModels app = new TestTangentGenBadModels();
app.start();
}
@Override
public void simpleInitApp() {
// assetManager.registerLocator("http://jme-glsl-shaders.googlecode.com/hg/assets/Models/LightBlow/", UrlLocator.class);
// assetManager.registerLocator("http://jmonkeyengine.googlecode.com/files/", UrlLocator.class);
final Spatial badModel = assetManager.loadModel("Models/TangentBugs/test.blend");
// badModel.setLocalScale(1f);
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
mat.setTexture("NormalMap", assetManager.loadTexture("Models/TangentBugs/test_normal.png"));
// Material mat = assetManager.loadMaterial("Textures/BumpMapTest/Tangent.j3m");
badModel.setMaterial(mat);
rootNode.attachChild(badModel);
// TODO: For some reason blender loader fails to load this.
// need to check it
// Spatial model = assetManager.loadModel("test.blend");
// rootNode.attachChild(model);
final Node debugTangents = new Node("debug tangents");
debugTangents.setCullHint(CullHint.Always);
rootNode.attachChild(debugTangents);
final Material debugMat = assetManager.loadMaterial("Common/Materials/VertexColor.j3m");
badModel.depthFirstTraversal(new SceneGraphVisitorAdapter(){
@Override
public void visit(Geometry g){
Mesh m = g.getMesh();
Material mat = g.getMaterial();
// if (mat.getParam("DiffuseMap") != null){
// mat.setTexture("DiffuseMap", null);
// }
TangentBinormalGenerator.generate(m);
Geometry debug = new Geometry(
"debug tangents geom",
TangentBinormalGenerator.genTbnLines(g.getMesh(), 0.2f)
);
debug.setMaterial(debugMat);
debug.setCullHint(Spatial.CullHint.Never);
debug.setLocalTransform(g.getWorldTransform());
debugTangents.attachChild(debug);
}
});
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(-0.8f, -0.6f, -0.08f).normalizeLocal());
dl.setColor(new ColorRGBA(1,1,1,1));
rootNode.addLight(dl);
lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f));
lightMdl.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
lightMdl.getMesh().setStatic();
rootNode.attachChild(lightMdl);
pl = new PointLight();
pl.setColor(ColorRGBA.White);
// rootNode.addLight(pl);
BitmapText info = new BitmapText(guiFont);
info.setText("Press SPACE to switch between lighting and tangent display");
info.setQueueBucket(Bucket.Gui);
info.move(0, settings.getHeight() - info.getLineHeight(), 0);
rootNode.attachChild(info);
inputManager.addMapping("space", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addListener(new ActionListener() {
private boolean isLit = true;
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if (isPressed) return;
Material mat;
if (isLit){
mat = assetManager.loadMaterial("Textures/BumpMapTest/Tangent.j3m");
debugTangents.setCullHint(CullHint.Inherit);
}else{
mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
mat.setTexture("NormalMap", assetManager.loadTexture("Models/TangentBugs/test_normal.png"));
debugTangents.setCullHint(CullHint.Always);
}
isLit = !isLit;
badModel.setMaterial(mat);
}
}, "space");
}
@Override
public void simpleUpdate(float tpf){
angle += tpf;
angle %= FastMath.TWO_PI;
pl.setPosition(new Vector3f(FastMath.cos(angle) * 2f, 2f, FastMath.sin(angle) * 2f));
lightMdl.setLocalTranslation(pl.getPosition());
}
}

@ -1,95 +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.
*/
package jme3test.model.anim;
import com.jme3.anim.AnimClip;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.util.AnimMigrationUtils;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.BlenderKey;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
public class TestBlenderAnim extends SimpleApplication {
public static void main(String[] args) {
TestBlenderAnim app = new TestBlenderAnim();
app.start();
}
@Override
public void simpleInitApp() {
flyCam.setMoveSpeed(10f);
cam.setLocation(new Vector3f(6.4013605f, 7.488437f, 12.843031f));
cam.setRotation(new Quaternion(-0.060740203f, 0.93925786f, -0.2398315f, -0.2378785f));
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(-0.1f, -0.7f, -1).normalizeLocal());
dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
rootNode.addLight(dl);
BlenderKey blenderKey = new BlenderKey("Blender/2.4x/BaseMesh_249.blend");
Spatial scene = assetManager.loadModel(blenderKey);
rootNode.attachChild(scene);
Spatial model = this.findNode(rootNode, "BaseMesh_01");
AnimMigrationUtils.migrate(model);
model.center();
AnimComposer animComposer = model.getControl(AnimComposer.class);
animComposer.getAnimClips().forEach(animClip -> System.out.println("AnimClip name: " + animClip.getName()));
AnimClip animClip = animComposer.getAnimClip("run_01"); // run_sideway_left, aim, run_sideway_right, base_stand, run_01, base, jump
animComposer.setCurrentAction(animClip.getName());
}
/**
* This method finds a node of a given name.
*
* @param rootNode
* the root node to search
* @param name
* the name of the searched node
* @return the found node or null
*/
private Spatial findNode(Node rootNode, String name) {
if (name.equals(rootNode.getName())) {
return rootNode;
}
return rootNode.getChild(name);
}
}

@ -1,94 +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.
*/
package jme3test.model.anim;
import com.jme3.anim.AnimClip;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.util.AnimMigrationUtils;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.BlenderKey;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
public class TestBlenderObjectAnim extends SimpleApplication {
public static void main(String[] args) {
TestBlenderObjectAnim app = new TestBlenderObjectAnim();
app.start();
}
@Override
public void simpleInitApp() {
flyCam.setMoveSpeed(10f);
cam.setLocation(new Vector3f(6.4013605f, 7.488437f, 12.843031f));
cam.setRotation(new Quaternion(-0.060740203f, 0.93925786f, -0.2398315f, -0.2378785f));
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(-0.1f, -0.7f, -1).normalizeLocal());
dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
rootNode.addLight(dl);
BlenderKey blenderKey = new BlenderKey("Blender/2.4x/animtest.blend");
Spatial scene = assetManager.loadModel(blenderKey);
rootNode.attachChild(scene);
Spatial model = this.findNode(rootNode, "Cube");
model.center();
// Because it's old .blend file need to migrate object.
AnimMigrationUtils.migrate(model);
AnimComposer animComposer = model.getControl(AnimComposer.class);
animComposer.getAnimClips().forEach(animClip -> System.out.println("AnimClip name: " + animClip.getName()));
AnimClip animClip = animComposer.getAnimClip("Action"); // Action, Action.001
animComposer.setCurrentAction(animClip.getName());
}
/**
* This method finds a node of a given name.
* @param rootNode the root node to search
* @param name the name of the searched node
* @return the found node or null
*/
private Spatial findNode(Node rootNode, String name) {
if (name.equals(rootNode.getName())) {
return rootNode;
}
return rootNode.getChild(name);
}
}

@ -1,733 +0,0 @@
/*
* 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;
}
}

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

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

@ -1,767 +0,0 @@
/*
* 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() + " ]";
}
}

@ -1,419 +0,0 @@
/*
* 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;
}
}
}

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

@ -1,391 +0,0 @@
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;
}
}

@ -1,134 +0,0 @@
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() + "]]";
}
}

@ -1,400 +0,0 @@
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;
}
}

@ -1,133 +0,0 @@
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 + ")]";
}
}

@ -1,317 +0,0 @@
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!");
}
}
}

@ -1,148 +0,0 @@
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;
}
}

@ -1,88 +0,0 @@
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;
}
}

@ -1,186 +0,0 @@
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 + ")";
}
}

@ -1,476 +0,0 @@
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!");
}
}
}
}

@ -1,397 +0,0 @@
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;
}
}

@ -1,41 +0,0 @@
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;
}
}

@ -1,32 +0,0 @@
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;
}
}

@ -1,165 +0,0 @@
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();
}
}

@ -1,162 +0,0 @@
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();
}
}

@ -1,84 +0,0 @@
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";
}
}

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

@ -1,236 +0,0 @@
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);
}
}
}

@ -1,107 +0,0 @@
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;
}
}

@ -1,106 +0,0 @@
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;
}
}

@ -1,61 +0,0 @@
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;
}
}

@ -1,34 +0,0 @@
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;
}
}

@ -1,87 +0,0 @@
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;
}
}

@ -1,129 +0,0 @@
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;
}
}

@ -1,74 +0,0 @@
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;
}
}

@ -1,96 +0,0 @@
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;
}
}

@ -1,81 +0,0 @@
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;
}
}

@ -1,40 +0,0 @@
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;
}
}

@ -1,172 +0,0 @@
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] + ")]";
}
}
}

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

@ -1,890 +0,0 @@
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;
}
}
}

@ -1,77 +0,0 @@
/*
* 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);
}
}

@ -1,371 +0,0 @@
/*
* 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
}
}

@ -1,203 +0,0 @@
/*
* 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();
}
}

@ -1,135 +0,0 @@
/*
* 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();
}
}

@ -1,327 +0,0 @@
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();
}
}

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

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

@ -1,328 +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.
*/
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.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();
}
}
}

@ -1,214 +0,0 @@
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);
}
}

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

@ -1,26 +0,0 @@
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);
}

@ -1,366 +0,0 @@
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!");
}
}

@ -1,387 +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.
*/
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;
}
}
}
}

@ -1,583 +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.
*/
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
}
}
}

@ -1,190 +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.
*/
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();
}
}
}

@ -1,210 +0,0 @@
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);
}
}

@ -1,869 +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.
*/
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);
}
}

@ -1,349 +0,0 @@
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;
}
}

@ -1,613 +0,0 @@
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);
}
}

@ -1,305 +0,0 @@
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);
}
}

@ -1,364 +0,0 @@
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.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;
}
}
}

@ -1,381 +0,0 @@
/*
* 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;
}
}

@ -1,93 +0,0 @@
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;
}
}

@ -1,767 +0,0 @@
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());
}
}

@ -1,113 +0,0 @@
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);
}
}
}
}

@ -1,241 +0,0 @@
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);
}
}
}
}

@ -1,200 +0,0 @@
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;
}
}

@ -1,196 +0,0 @@
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));
}
}

@ -1,92 +0,0 @@
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;
}
}

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

@ -1,107 +0,0 @@
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);
}
}
}
}

@ -1,642 +0,0 @@
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 + "]";
}
}
}

@ -1,54 +0,0 @@
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);
}
}
}

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

@ -1,365 +0,0 @@
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;
}
}

@ -1,192 +0,0 @@
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;
}
}

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

@ -1,543 +0,0 @@
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;
}
}

@ -1,157 +0,0 @@
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;
}
}

@ -1,282 +0,0 @@
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);
}
}

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

@ -1,473 +0,0 @@
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);
}
}

@ -1,691 +0,0 @@
/*
* 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;
}
}

@ -1,392 +0,0 @@
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 + "}]";
}
}

@ -1,662 +0,0 @@
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;
}
}

@ -1,420 +0,0 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.plugins.blender.textures.UVProjectionGenerator.UVProjectionType;
import com.jme3.util.BufferUtils;
/**
* This class is used for UV coordinates generation.
*
* @author Marcin Roguski (Kaelthas)
*/
public class UVCoordinatesGenerator {
private static final Logger LOGGER = Logger.getLogger(UVCoordinatesGenerator.class.getName());
public static enum UVCoordinatesType {
TEXCO_ORCO(1), TEXCO_REFL(2), TEXCO_NORM(4), TEXCO_GLOB(8), TEXCO_UV(16), TEXCO_OBJECT(32), TEXCO_LAVECTOR(64), TEXCO_VIEW(128),
TEXCO_STICKY(256), TEXCO_OSA(512), TEXCO_WINDOW(1024), NEED_UV(2048), TEXCO_TANGENT(4096),
TEXCO_PARTICLE_OR_STRAND(8192), //TEXCO_PARTICLE (since blender 2.6x) has also the value of: 8192 but is used for halo materials instead of normal materials
TEXCO_STRESS(16384), TEXCO_SPEED(32768);
public final int blenderValue;
private UVCoordinatesType(int blenderValue) {
this.blenderValue = blenderValue;
}
public static UVCoordinatesType valueOf(int blenderValue) {
for (UVCoordinatesType coordinatesType : UVCoordinatesType.values()) {
if (coordinatesType.blenderValue == blenderValue) {
return coordinatesType;
}
}
return null;
}
}
/**
* Generates a UV coordinates for 2D texture.
*
* @param mesh
* the mesh we generate UV's for
* @param texco
* UV coordinates type
* @param projection
* projection type
* @param geometries
* the geometris the given mesh belongs to (required to compute
* bounding box)
* @return UV coordinates for the given mesh
*/
public static List<Vector2f> generateUVCoordinatesFor2DTexture(Mesh mesh, UVCoordinatesType texco, UVProjectionType projection, Geometry geometries) {
List<Vector2f> result = new ArrayList<Vector2f>();
BoundingBox bb = UVCoordinatesGenerator.getBoundingBox(geometries);
float[] inputData = null;// positions, normals, reflection vectors, etc.
switch (texco) {
case TEXCO_ORCO:
inputData = BufferUtils.getFloatArray(mesh.getFloatBuffer(VertexBuffer.Type.Position));
break;
case TEXCO_UV:// this should be used if not defined by user explicitly
Vector2f[] data = new Vector2f[] { new Vector2f(0, 1), new Vector2f(0, 0), new Vector2f(1, 0) };
for (int i = 0; i < mesh.getVertexCount(); ++i) {
result.add(data[i % 3]);
}
break;
case TEXCO_NORM:
inputData = BufferUtils.getFloatArray(mesh.getFloatBuffer(VertexBuffer.Type.Normal));
break;
case TEXCO_REFL:
case TEXCO_GLOB:
case TEXCO_TANGENT:
case TEXCO_STRESS:
case TEXCO_LAVECTOR:
case TEXCO_OBJECT:
case TEXCO_OSA:
case TEXCO_PARTICLE_OR_STRAND:
case TEXCO_SPEED:
case TEXCO_STICKY:
case TEXCO_VIEW:
case TEXCO_WINDOW:
LOGGER.warning("Texture coordinates type not currently supported: " + texco);
break;
default:
throw new IllegalStateException("Unknown texture coordinates value: " + texco);
}
if (inputData != null) {// make projection calculations
switch (projection) {
case PROJECTION_FLAT:
inputData = UVProjectionGenerator.flatProjection(inputData, bb);
break;
case PROJECTION_CUBE:
inputData = UVProjectionGenerator.cubeProjection(inputData, bb);
break;
case PROJECTION_TUBE:
BoundingTube bt = UVCoordinatesGenerator.getBoundingTube(geometries);
inputData = UVProjectionGenerator.tubeProjection(inputData, bt);
break;
case PROJECTION_SPHERE:
BoundingSphere bs = UVCoordinatesGenerator.getBoundingSphere(geometries);
inputData = UVProjectionGenerator.sphereProjection(inputData, bs);
break;
default:
throw new IllegalStateException("Unknown projection type: " + projection);
}
for (int i = 0; i < inputData.length; i += 2) {
result.add(new Vector2f(inputData[i], inputData[i + 1]));
}
}
return result;
}
/**
* Generates a UV coordinates for 3D texture.
*
* @param mesh
* the mesh we generate UV's for
* @param texco
* UV coordinates type
* @param coordinatesSwappingIndexes
* coordinates swapping indexes
* @param geometries
* the geometris the given mesh belongs to (required to compute
* bounding box)
* @return UV coordinates for the given mesh
*/
public static List<Vector3f> generateUVCoordinatesFor3DTexture(Mesh mesh, UVCoordinatesType texco, int[] coordinatesSwappingIndexes, Geometry... geometries) {
List<Vector3f> result = new ArrayList<Vector3f>();
BoundingBox bb = UVCoordinatesGenerator.getBoundingBox(geometries);
float[] inputData = null;// positions, normals, reflection vectors, etc.
switch (texco) {
case TEXCO_ORCO:
inputData = BufferUtils.getFloatArray(mesh.getFloatBuffer(VertexBuffer.Type.Position));
break;
case TEXCO_UV:
Vector2f[] data = new Vector2f[] { new Vector2f(0, 1), new Vector2f(0, 0), new Vector2f(1, 0) };
for (int i = 0; i < mesh.getVertexCount(); ++i) {
Vector2f uv = data[i % 3];
result.add(new Vector3f(uv.x, uv.y, 0));
}
break;
case TEXCO_NORM:
inputData = BufferUtils.getFloatArray(mesh.getFloatBuffer(VertexBuffer.Type.Normal));
break;
case TEXCO_REFL:
case TEXCO_GLOB:
case TEXCO_TANGENT:
case TEXCO_STRESS:
case TEXCO_LAVECTOR:
case TEXCO_OBJECT:
case TEXCO_OSA:
case TEXCO_PARTICLE_OR_STRAND:
case TEXCO_SPEED:
case TEXCO_STICKY:
case TEXCO_VIEW:
case TEXCO_WINDOW:
LOGGER.warning("Texture coordinates type not currently supported: " + texco);
break;
default:
throw new IllegalStateException("Unknown texture coordinates value: " + texco);
}
if (inputData != null) {// make calculations
Vector3f min = bb.getMin(null);
float[] uvCoordsResults = new float[4];// used for coordinates swapping
float[] ext = new float[] { bb.getXExtent() * 2, bb.getYExtent() * 2, bb.getZExtent() * 2 };
for (int i = 0; i < ext.length; ++i) {
if (ext[i] == 0) {
ext[i] = 1;
}
}
// now transform the coordinates so that they are in the range of
// <0; 1>
for (int i = 0; i < inputData.length; i += 3) {
uvCoordsResults[1] = (inputData[i] - min.x) / ext[0];
uvCoordsResults[2] = (inputData[i + 1] - min.y) / ext[1];
uvCoordsResults[3] = (inputData[i + 2] - min.z) / ext[2];
result.add(new Vector3f(uvCoordsResults[coordinatesSwappingIndexes[0]], uvCoordsResults[coordinatesSwappingIndexes[1]], uvCoordsResults[coordinatesSwappingIndexes[2]]));
}
}
return result;
}
/**
* This method should be used to determine if the texture will ever be
* computed. If the texture coordinates are not supported then the try of
* flattening the texture might result in runtime exceptions occurence.
*
* @param texco
* the texture coordinates type
* @return <b>true</b> if the type is supported and false otherwise
*/
public static boolean isTextureCoordinateTypeSupported(UVCoordinatesType texco) {
switch (texco) {
case TEXCO_ORCO:
case TEXCO_UV:
case TEXCO_NORM:
return true;
case TEXCO_REFL:
case TEXCO_GLOB:
case TEXCO_TANGENT:
case TEXCO_STRESS:
case TEXCO_LAVECTOR:
case TEXCO_OBJECT:
case TEXCO_OSA:
case TEXCO_PARTICLE_OR_STRAND:
case TEXCO_SPEED:
case TEXCO_STICKY:
case TEXCO_VIEW:
case TEXCO_WINDOW:
return false;
default:
throw new IllegalStateException("Unknown texture coordinates value: " + texco);
}
}
/**
* This method returns the bounding box of the given geometries.
*
* @param geometries
* the list of geometries
* @return bounding box of the given geometries
*/
public static BoundingBox getBoundingBox(Geometry... geometries) {
BoundingBox result = null;
for (Geometry geometry : geometries) {
geometry.updateModelBound();
BoundingVolume bv = geometry.getModelBound();
if (bv instanceof BoundingBox) {
return (BoundingBox) bv;
} else if (bv instanceof BoundingSphere) {
BoundingSphere bs = (BoundingSphere) bv;
float r = bs.getRadius();
return new BoundingBox(bs.getCenter(), r, r, r);
} else {
throw new IllegalStateException("Unknown bounding volume type: " + bv.getClass().getName());
}
}
return result;
}
/**
* This method returns the bounding sphere of the given geometries.
*
* @param geometries
* the list of geometries
* @return bounding sphere of the given geometries
*/
/* package */static BoundingSphere getBoundingSphere(Geometry... geometries) {
BoundingSphere result = null;
for (Geometry geometry : geometries) {
geometry.updateModelBound();
BoundingVolume bv = geometry.getModelBound();
if (bv instanceof BoundingBox) {
BoundingBox bb = (BoundingBox) bv;
float r = Math.max(bb.getXExtent(), bb.getYExtent());
r = Math.max(r, bb.getZExtent());
return new BoundingSphere(r, bb.getCenter());
} else if (bv instanceof BoundingSphere) {
return (BoundingSphere) bv;
} else {
throw new IllegalStateException("Unknown bounding volume type: " + bv.getClass().getName());
}
}
return result;
}
/**
* This method returns the bounding tube of the given geometries.
*
* @param geometries
* the list of geometries
* @return bounding tube of the given geometries
*/
/* package */static BoundingTube getBoundingTube(Geometry... geometries) {
BoundingTube result = null;
for (Geometry geometry : geometries) {
BoundingBox bb = UVCoordinatesGenerator.getBoundingBox(geometry);
Vector3f max = bb.getMax(null);
Vector3f min = bb.getMin(null);
float radius = Math.max(max.x - min.x, max.y - min.y) * 0.5f;
BoundingTube bt = new BoundingTube(radius, max.z - min.z, bb.getCenter());
if (result == null) {
result = bt;
} else {
result.merge(bt);
}
}
return result;
}
/**
* A very simple bounding tube. It holds only the basic data bout the
* bounding tube and does not provide full functionality of a
* BoundingVolume. Should be replaced with a bounding tube that extends the
* BoundingVolume if it is ever created.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */static class BoundingTube {
private float radius;
private float height;
private Vector3f center;
/**
* Constructor creates the tube with the given params.
*
* @param radius
* the radius of the tube
* @param height
* the height of the tube
* @param center
* the center of the tube
*/
public BoundingTube(float radius, float height, Vector3f center) {
this.radius = radius;
this.height = height;
this.center = center;
}
/**
* This method merges two bounding tubes.
*
* @param boundingTube
* bounding tube to be merged woth the current one
* @return new instance of bounding tube representing the tubes' merge
*/
public BoundingTube merge(BoundingTube boundingTube) {
// get tubes (tube1.radius >= tube2.radius)
BoundingTube tube1, tube2;
if (radius >= boundingTube.radius) {
tube1 = this;
tube2 = boundingTube;
} else {
tube1 = boundingTube;
tube2 = this;
}
float r1 = tube1.radius;
float r2 = tube2.radius;
float minZ = Math.min(tube1.center.z - tube1.height * 0.5f, tube2.center.z - tube2.height * 0.5f);
float maxZ = Math.max(tube1.center.z + tube1.height * 0.5f, tube2.center.z + tube2.height * 0.5f);
float height = maxZ - minZ;
Vector3f distance = tube2.center.subtract(tube1.center);
Vector3f center = tube1.center.add(distance.mult(0.5f));
distance.z = 0;// projecting this vector on XY plane
float d = distance.length();
// d <= r1 - r2: tube2 is inside tube1 or touches tube1 from the
// inside
// d > r1 - r2: tube2 is outside or touches tube1 or crosses tube1
float radius = d <= r1 - r2 ? tube1.radius : (d + r1 + r2) * 0.5f;
return new BoundingTube(radius, height, center);
}
/**
* @return the radius of the tube
*/
public float getRadius() {
return radius;
}
/**
* @return the height of the tube
*/
public float getHeight() {
return height;
}
/**
* @return the center of the tube
*/
public Vector3f getCenter() {
return center;
}
}
}

@ -1,240 +0,0 @@
package com.jme3.scene.plugins.blender.textures;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.math.FastMath;
import com.jme3.math.Triangle;
import com.jme3.math.Vector3f;
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.BoundingTube;
/**
* This class helps with projection calculations.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class UVProjectionGenerator {
/**
* 2D texture mapping (projection)
* @author Marcin Roguski (Kaelthas)
*/
public static enum UVProjectionType {
PROJECTION_FLAT(0), PROJECTION_CUBE(1), PROJECTION_TUBE(2), PROJECTION_SPHERE(3);
public final int blenderValue;
private UVProjectionType(int blenderValue) {
this.blenderValue = blenderValue;
}
public static UVProjectionType valueOf(int blenderValue) {
for (UVProjectionType projectionType : UVProjectionType.values()) {
if (projectionType.blenderValue == blenderValue) {
return projectionType;
}
}
return null;
}
}
/**
* Flat projection for 2D textures.
*
* @param mesh
* mesh that is to be projected
* @param bb
* the bounding box for projecting
* @return UV coordinates after the projection
*/
public static float[] flatProjection(float[] positions, BoundingBox bb) {
Vector3f min = bb.getMin(null);
float[] ext = new float[] { bb.getXExtent() * 2.0f, bb.getZExtent() * 2.0f };
float[] uvCoordinates = new float[positions.length / 3 * 2];
for (int i = 0, j = 0; i < positions.length; i += 3, j += 2) {
uvCoordinates[j] = (positions[i] - min.x) / ext[0];
// skip the Y-coordinate
uvCoordinates[j + 1] = (positions[i + 2] - min.z) / ext[1];
}
return uvCoordinates;
}
/**
* Cube projection for 2D textures.
*
* @param positions
* points to be projected
* @param bb
* the bounding box for projecting
* @return UV coordinates after the projection
*/
public static float[] cubeProjection(float[] positions, BoundingBox bb) {
Triangle triangle = new Triangle();
Vector3f x = new Vector3f(1, 0, 0);
Vector3f y = new Vector3f(0, 1, 0);
Vector3f z = new Vector3f(0, 0, 1);
Vector3f min = bb.getMin(null);
float[] ext = new float[] { bb.getXExtent() * 2.0f, bb.getYExtent() * 2.0f, bb.getZExtent() * 2.0f };
float[] uvCoordinates = new float[positions.length / 3 * 2];
float borderAngle = (float) Math.sqrt(2.0f) / 2.0f;
for (int i = 0, pointIndex = 0; i < positions.length; i += 9) {
triangle.set(0, positions[i], positions[i + 1], positions[i + 2]);
triangle.set(1, positions[i + 3], positions[i + 4], positions[i + 5]);
triangle.set(2, positions[i + 6], positions[i + 7], positions[i + 8]);
Vector3f n = triangle.getNormal();
float dotNX = Math.abs(n.dot(x));
float dorNY = Math.abs(n.dot(y));
float dotNZ = Math.abs(n.dot(z));
if (dotNX > borderAngle) {
if (dotNZ < borderAngle) {// discard X-coordinate
uvCoordinates[pointIndex++] = (triangle.get1().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get1().z - min.z) / ext[2];
uvCoordinates[pointIndex++] = (triangle.get2().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get2().z - min.z) / ext[2];
uvCoordinates[pointIndex++] = (triangle.get3().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get3().z - min.z) / ext[2];
} else {// discard Z-coordinate
uvCoordinates[pointIndex++] = (triangle.get1().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get1().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get2().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get2().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get3().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get3().y - min.y) / ext[1];
}
} else {
if (dorNY > borderAngle) {// discard Y-coordinate
uvCoordinates[pointIndex++] = (triangle.get1().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get1().z - min.z) / ext[2];
uvCoordinates[pointIndex++] = (triangle.get2().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get2().z - min.z) / ext[2];
uvCoordinates[pointIndex++] = (triangle.get3().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get3().z - min.z) / ext[2];
} else {// discard Z-coordinate
uvCoordinates[pointIndex++] = (triangle.get1().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get1().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get2().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get2().y - min.y) / ext[1];
uvCoordinates[pointIndex++] = (triangle.get3().x - min.x) / ext[0];
uvCoordinates[pointIndex++] = (triangle.get3().y - min.y) / ext[1];
}
}
triangle.setNormal(null);// clear the previous normal vector
}
return uvCoordinates;
}
/**
* Tube projection for 2D textures.
*
* @param positions
* points to be projected
* @param bt
* the bounding tube for projecting
* @return UV coordinates after the projection
*/
public static float[] tubeProjection(float[] positions, BoundingTube bt) {
float[] uvCoordinates = new float[positions.length / 3 * 2];
Vector3f v = new Vector3f();
float cx = bt.getCenter().x, cz = bt.getCenter().z;
Vector3f uBase = new Vector3f(0, 0, -1);
float vBase = bt.getCenter().y - bt.getHeight() * 0.5f;
for (int i = 0, j = 0; i < positions.length; i += 3, j += 2) {
// calculating U
v.set(positions[i] - cx, 0, positions[i + 2] - cz);
v.normalizeLocal();
float angle = v.angleBetween(uBase);// result between [0; PI]
if (v.x < 0) {// the angle should be greater than PI, we're on the other part of the image then
angle = FastMath.TWO_PI - angle;
}
uvCoordinates[j] = angle / FastMath.TWO_PI;
// calculating V
float y = positions[i + 1];
uvCoordinates[j + 1] = (y - vBase) / bt.getHeight();
}
// looking for splitted triangles
Triangle triangle = new Triangle();
for (int i = 0; i < positions.length; i += 9) {
triangle.set(0, positions[i], positions[i + 1], positions[i + 2]);
triangle.set(1, positions[i + 3], positions[i + 4], positions[i + 5]);
triangle.set(2, positions[i + 6], positions[i + 7], positions[i + 8]);
float sgn1 = Math.signum(triangle.get1().x - cx);
float sgn2 = Math.signum(triangle.get2().x - cx);
float sgn3 = Math.signum(triangle.get3().x - cx);
float xSideFactor = sgn1 + sgn2 + sgn3;
float ySideFactor = Math.signum(triangle.get1().z - cz) + Math.signum(triangle.get2().z - cz) + Math.signum(triangle.get3().z - cz);
if ((xSideFactor > -3 || xSideFactor < 3) && ySideFactor < 0) {// the triangle is on the splitting plane
if (sgn1 == 1.0f) {
uvCoordinates[i / 3 * 2] += 1.0f;
}
if (sgn2 == 1.0f) {
uvCoordinates[(i / 3 + 1) * 2] += 1.0f;
}
if (sgn3 == 1.0f) {
uvCoordinates[(i / 3 + 2) * 2] += 1.0f;
}
}
}
return uvCoordinates;
}
/**
* Sphere projection for 2D textures.
*
* @param positions
* points to be projected
* @param bb
* the bounding box for projecting
* @return UV coordinates after the projection
*/
public static float[] sphereProjection(float[] positions, BoundingSphere bs) {// TODO: rotate it to be vertical
float[] uvCoordinates = new float[positions.length / 3 * 2];
Vector3f v = new Vector3f();
float cx = bs.getCenter().x, cy = bs.getCenter().y, cz = bs.getCenter().z;
Vector3f uBase = new Vector3f(0, -1, 0);
Vector3f vBase = new Vector3f(0, 0, -1);
for (int i = 0, j = 0; i < positions.length; i += 3, j += 2) {
// calculating U
v.set(positions[i] - cx, positions[i + 1] - cy, 0);
v.normalizeLocal();
float angle = v.angleBetween(uBase);// result between [0; PI]
if (v.x < 0) {// the angle should be greater than PI, we're on the other part of the image then
angle = FastMath.TWO_PI - angle;
}
uvCoordinates[j] = angle / FastMath.TWO_PI;
// calculating V
v.set(positions[i] - cx, positions[i + 1] - cy, positions[i + 2] - cz);
v.normalizeLocal();
angle = v.angleBetween(vBase);// result between [0; PI]
uvCoordinates[j + 1] = angle / FastMath.PI;
}
// looking for splitted triangles
Triangle triangle = new Triangle();
for (int i = 0; i < positions.length; i += 9) {
triangle.set(0, positions[i], positions[i + 1], positions[i + 2]);
triangle.set(1, positions[i + 3], positions[i + 4], positions[i + 5]);
triangle.set(2, positions[i + 6], positions[i + 7], positions[i + 8]);
float sgn1 = Math.signum(triangle.get1().x - cx);
float sgn2 = Math.signum(triangle.get2().x - cx);
float sgn3 = Math.signum(triangle.get3().x - cx);
float xSideFactor = sgn1 + sgn2 + sgn3;
float ySideFactor = Math.signum(triangle.get1().y - cy) + Math.signum(triangle.get2().y - cy) + Math.signum(triangle.get3().y - cy);
if ((xSideFactor > -3 || xSideFactor < 3) && ySideFactor < 0) {// the triangle is on the splitting plane
if (sgn1 == 1.0f) {
uvCoordinates[i / 3 * 2] += 1.0f;
}
if (sgn2 == 1.0f) {
uvCoordinates[(i / 3 + 1) * 2] += 1.0f;
}
if (sgn3 == 1.0f) {
uvCoordinates[(i / 3 + 2) * 2] += 1.0f;
}
}
}
return uvCoordinates;
}
}

@ -1,86 +0,0 @@
package com.jme3.scene.plugins.blender.textures;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.jme3.math.Vector2f;
/**
* A collection of UV coordinates. The coords are stored in groups defined by the material index and their UV set name.
*
* @author Kaelthas (Marcin Roguski)
*/
public class UserUVCollection {
/** A map between material number and UV coordinates of mesh that has this material applied. */
private Map<Integer, LinkedHashMap<String, List<Vector2f>>> uvCoordinates = new HashMap<Integer, LinkedHashMap<String, List<Vector2f>>>();
/** A map between vertex index and its UV coordinates. */
private Map<String, Map<Integer, Vector2f>> uvsMap = new HashMap<String, Map<Integer, Vector2f>>();
/**
* Adds a single UV coordinates for a specified vertex index.
* @param materialIndex
* the material index
* @param uvSetName
* the UV set name
* @param uv
* the added UV coordinates
* @param jmeVertexIndex
* the index of the vertex in result jme mesh
*/
public void addUV(int materialIndex, String uvSetName, Vector2f uv, int jmeVertexIndex) {
// first get all UV sets for the specified material ...
LinkedHashMap<String, List<Vector2f>> uvsForMaterial = uvCoordinates.get(materialIndex);
if (uvsForMaterial == null) {
uvsForMaterial = new LinkedHashMap<String, List<Vector2f>>();
uvCoordinates.put(materialIndex, uvsForMaterial);
}
// ... then fetch the UVS for the specified UV set name ...
List<Vector2f> uvsForName = uvsForMaterial.get(uvSetName);
if (uvsForName == null) {
uvsForName = new ArrayList<Vector2f>();
uvsForMaterial.put(uvSetName, uvsForName);
}
// ... add the UV coordinates to the proper list ...
uvsForName.add(uv);
// ... and add the mapping of the UV coordinates to a vertex index for the specified UV set
Map<Integer, Vector2f> uvToVertexIndexMapping = uvsMap.get(uvSetName);
if (uvToVertexIndexMapping == null) {
uvToVertexIndexMapping = new HashMap<Integer, Vector2f>();
uvsMap.put(uvSetName, uvToVertexIndexMapping);
}
uvToVertexIndexMapping.put(jmeVertexIndex, uv);
}
/**
* @param uvSetName
* the name of the UV set
* @param vertexIndex
* the vertex index corresponds to the index in jme mesh and not the original one in blender
* @return a pre-existing coordinate vector
*/
public Vector2f getUVForVertex(String uvSetName, int vertexIndex) {
return uvsMap.get(uvSetName).get(vertexIndex);
}
/**
* @param materialNumber
* the material number that is appied to the mesh
* @return UV coordinates of vertices that belong to the required mesh part
*/
public LinkedHashMap<String, List<Vector2f>> getUVCoordinates(int materialNumber) {
return uvCoordinates.get(materialNumber);
}
/**
* @return indicates if the mesh has UV coordinates
*/
public boolean hasUVCoordinates() {
return uvCoordinates.size() > 0;
}
}

@ -1,139 +0,0 @@
package com.jme3.scene.plugins.blender.textures.blending;
import java.util.logging.Logger;
import jme3tools.converters.MipMapGenerator;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.texture.Image;
/**
* An abstract class that contains the basic methods used by the classes that
* will derive from it.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */abstract class AbstractTextureBlender implements TextureBlender {
private static final Logger LOGGER = Logger.getLogger(AbstractTextureBlender.class.getName());
protected int flag;
protected boolean negateTexture;
protected int blendType;
protected float[] materialColor;
protected float[] color;
protected float blendFactor;
public AbstractTextureBlender(int flag, boolean negateTexture, int blendType, float[] materialColor, float[] color, float blendFactor) {
this.flag = flag;
this.negateTexture = negateTexture;
this.blendType = blendType;
this.materialColor = materialColor;
this.color = color;
this.blendFactor = blendFactor;
}
/**
* The method that performs the ramp blending.
*
* @param type
* the blend type
* @param materialRGB
* the rgb value of the material, here the result is stored too
* @param fac
* color affection factor
* @param pixelColor
* the texture color
* @param blenderContext
* the blender context
*/
protected void blendHSV(int type, float[] materialRGB, float fac, float[] pixelColor, BlenderContext blenderContext) {
float oneMinusFactor = 1.0f - fac;
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
switch (type) {
case MTEX_BLEND_HUE: {// FIXME: not working well for image textures (works fine for generated textures)
float[] colorTransformResult = new float[3];
materialHelper.rgbToHsv(pixelColor[0], pixelColor[1], pixelColor[2], colorTransformResult);
if (colorTransformResult[0] != 0.0f) {
float colH = colorTransformResult[0];
materialHelper.rgbToHsv(materialRGB[0], materialRGB[1], materialRGB[2], colorTransformResult);
materialHelper.hsvToRgb(colH, colorTransformResult[1], colorTransformResult[2], colorTransformResult);
materialRGB[0] = oneMinusFactor * materialRGB[0] + fac * colorTransformResult[0];
materialRGB[1] = oneMinusFactor * materialRGB[1] + fac * colorTransformResult[1];
materialRGB[2] = oneMinusFactor * materialRGB[2] + fac * colorTransformResult[2];
}
break;
}
case MTEX_BLEND_SAT: {
float[] colorTransformResult = new float[3];
materialHelper.rgbToHsv(materialRGB[0], materialRGB[1], materialRGB[2], colorTransformResult);
float h = colorTransformResult[0];
float s = colorTransformResult[1];
float v = colorTransformResult[2];
if (s != 0.0f) {
materialHelper.rgbToHsv(pixelColor[0], pixelColor[1], pixelColor[2], colorTransformResult);
materialHelper.hsvToRgb(h, oneMinusFactor * s + fac * colorTransformResult[1], v, materialRGB);
}
break;
}
case MTEX_BLEND_VAL: {
float[] rgbToHsv = new float[3];
float[] colToHsv = new float[3];
materialHelper.rgbToHsv(materialRGB[0], materialRGB[1], materialRGB[2], rgbToHsv);
materialHelper.rgbToHsv(pixelColor[0], pixelColor[1], pixelColor[2], colToHsv);
materialHelper.hsvToRgb(rgbToHsv[0], rgbToHsv[1], oneMinusFactor * rgbToHsv[2] + fac * colToHsv[2], materialRGB);
break;
}
case MTEX_BLEND_COLOR: {// FIXME: not working well for image textures (works fine for generated textures)
float[] rgbToHsv = new float[3];
float[] colToHsv = new float[3];
materialHelper.rgbToHsv(pixelColor[0], pixelColor[1], pixelColor[2], colToHsv);
if (colToHsv[2] != 0) {
materialHelper.rgbToHsv(materialRGB[0], materialRGB[1], materialRGB[2], rgbToHsv);
materialHelper.hsvToRgb(colToHsv[0], colToHsv[1], rgbToHsv[2], rgbToHsv);
materialRGB[0] = oneMinusFactor * materialRGB[0] + fac * rgbToHsv[0];
materialRGB[1] = oneMinusFactor * materialRGB[1] + fac * rgbToHsv[1];
materialRGB[2] = oneMinusFactor * materialRGB[2] + fac * rgbToHsv[2];
}
break;
}
default:
throw new IllegalStateException("Unknown ramp type: " + type);
}
}
@Override
public void copyBlendingData(TextureBlender textureBlender) {
if (textureBlender instanceof AbstractTextureBlender) {
flag = ((AbstractTextureBlender) textureBlender).flag;
negateTexture = ((AbstractTextureBlender) textureBlender).negateTexture;
blendType = ((AbstractTextureBlender) textureBlender).blendType;
materialColor = ((AbstractTextureBlender) textureBlender).materialColor.clone();
color = ((AbstractTextureBlender) textureBlender).color.clone();
blendFactor = ((AbstractTextureBlender) textureBlender).blendFactor;
} else {
LOGGER.warning("Cannot copy blending data from other types than " + this.getClass());
}
}
/**
* The method prepares images for blending. It generates mipmaps if one of
* the images has them defined and the other one has not.
*
* @param target
* the image where the blending result is stored
* @param source
* the image that is being read only
*/
protected void prepareImagesForBlending(Image target, Image source) {
LOGGER.fine("Generating mipmaps if needed!");
boolean targetHasMipmaps = target == null ? false : target.getMipMapSizes() != null && target.getMipMapSizes().length > 0;
boolean sourceHasMipmaps = source == null ? false : source.getMipMapSizes() != null && source.getMipMapSizes().length > 0;
if (target != null && !targetHasMipmaps && sourceHasMipmaps) {
MipMapGenerator.generateMipMaps(target);
} else if (source != null && !sourceHasMipmaps && targetHasMipmaps) {
MipMapGenerator.generateMipMaps(source);
}
}
}

@ -1,53 +0,0 @@
package com.jme3.scene.plugins.blender.textures.blending;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.texture.Image;
/**
* An interface for texture blending classes (the classes that mix the texture
* pixels with the material colors).
*
* @author Marcin Roguski (Kaelthas)
*/
public interface TextureBlender {
// types of blending
int MTEX_BLEND = 0;
int MTEX_MUL = 1;
int MTEX_ADD = 2;
int MTEX_SUB = 3;
int MTEX_DIV = 4;
int MTEX_DARK = 5;
int MTEX_DIFF = 6;
int MTEX_LIGHT = 7;
int MTEX_SCREEN = 8;
int MTEX_OVERLAY = 9;
int MTEX_BLEND_HUE = 10;
int MTEX_BLEND_SAT = 11;
int MTEX_BLEND_VAL = 12;
int MTEX_BLEND_COLOR = 13;
int MTEX_NUM_BLENDTYPES = 14;
/**
* This method blends the given texture with material color and the defined
* color in 'map to' panel. As a result of this method a new texture is
* created. The input texture is NOT.
*
* @param image
* the image we use in blending
* @param baseImage
* the texture that is underneath the current texture (its pixels
* will be used instead of material color)
* @param blenderContext
* the blender context
* @return new image that was created after the blending
*/
Image blend(Image image, Image baseImage, BlenderContext blenderContext);
/**
* Copies blending data. Used for blending type format changing.
*
* @param textureBlender
* the blend data that should be copied
*/
void copyBlendingData(TextureBlender textureBlender);
}

@ -1,275 +0,0 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures.blending;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.BlenderContext;
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.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* <p>
* The class that is responsible for blending the following texture types:
* </p>
* <ul>
* <li>RGBA8</li>
* <li>ABGR8</li>
* <li>BGR8</li>
* <li>RGB8</li>
* </ul>
*
* <p>
* Not yet supported (but will be):
* </p>
* <ul>
* <li>ARGB4444</li>
* <li>RGB10</li>
* <li>RGB111110F</li>
* <li>RGB16</li>
* <li>RGB16F</li>
* <li>RGB16F_to_RGB111110F</li>
* <li>RGB16F_to_RGB9E5</li>
* <li>RGB32F</li>
* <li>RGB565</li>
* <li>RGB5A1</li>
* <li>RGB9E5</li>
* <li>RGBA16</li>
* <li>RGBA16F</li>
* </ul>
*
* @author Marcin Roguski (Kaelthas)
*/
public class TextureBlenderAWT extends AbstractTextureBlender {
public TextureBlenderAWT(int flag, boolean negateTexture, int blendType, float[] materialColor, float[] color, float blendFactor) {
super(flag, negateTexture, blendType, materialColor, color, blendFactor);
}
@Override
public Image blend(Image image, Image baseImage, BlenderContext blenderContext) {
this.prepareImagesForBlending(image, baseImage);
float[] pixelColor = new float[] { color[0], color[1], color[2], 1.0f };
Format format = image.getFormat();
PixelInputOutput basePixelIO = null, pixelReader = PixelIOFactory.getPixelIO(format);
TexturePixel basePixel = null, pixel = new TexturePixel();
float[] materialColor = this.materialColor;
if (baseImage != null) {
basePixelIO = PixelIOFactory.getPixelIO(baseImage.getFormat());
materialColor = new float[this.materialColor.length];
basePixel = new TexturePixel();
}
int width = image.getWidth();
int height = image.getHeight();
int depth = image.getDepth();
if (depth == 0) {
depth = 1;
}
int bytesPerPixel = image.getFormat().getBitsPerPixel() >> 3;
ArrayList<ByteBuffer> dataArray = new ArrayList<ByteBuffer>(depth);
float[] resultPixel = new float[4];
for (int dataLayerIndex = 0; dataLayerIndex < depth; ++dataLayerIndex) {
ByteBuffer data = image.getData(dataLayerIndex);
data.rewind();
int imagePixelCount = data.limit() / bytesPerPixel;
ByteBuffer newData = BufferUtils.createByteBuffer(imagePixelCount * 4);
int dataIndex = 0, x = 0, y = 0, index = 0;
while (index < data.limit()) {
// getting the proper material color if the base texture is applied
if (basePixelIO != null) {
basePixelIO.read(baseImage, dataLayerIndex, basePixel, x, y);
basePixel.toRGBA(materialColor);
++x;
if (x >= width) {
x = 0;
++y;
}
}
// reading the current texture's pixel
pixelReader.read(image, dataLayerIndex, pixel, index);
index += bytesPerPixel;
pixel.toRGBA(pixelColor);
if (negateTexture) {
pixel.negate();
}
this.blendPixel(resultPixel, materialColor, pixelColor, blenderContext);
newData.put(dataIndex++, (byte) (resultPixel[0] * 255.0f));
newData.put(dataIndex++, (byte) (resultPixel[1] * 255.0f));
newData.put(dataIndex++, (byte) (resultPixel[2] * 255.0f));
newData.put(dataIndex++, (byte) (pixelColor[3] * 255.0f));
}
dataArray.add(newData);
}
ColorSpace colorSpace;
if (baseImage != null) {
colorSpace = baseImage.getColorSpace() != null ? baseImage.getColorSpace() : ColorSpace.Linear;
} else {
colorSpace = image.getColorSpace();
}
Image result = depth > 1 ? new Image(Format.RGBA8, width, height, depth, dataArray, colorSpace) : new Image(Format.RGBA8, width, height, dataArray.get(0), colorSpace);
if (image.getMipMapSizes() != null) {
result.setMipMapSizes(image.getMipMapSizes().clone());
}
return result;
}
/**
* This method blends the single pixel depending on the blending type.
*
* @param result
* the result pixel
* @param materialColor
* the material color
* @param pixelColor
* the pixel color
* @param blenderContext
* the blender context
*/
protected void blendPixel(float[] result, float[] materialColor, float[] pixelColor, BlenderContext blenderContext) {
// We calculate first the importance of the texture (colFactor * texAlphaValue)
float blendFactor = this.blendFactor * pixelColor[3];
// Then, we get the object material factor ((1 - texture importance) * matAlphaValue)
float oneMinusFactor = (1f - blendFactor) * materialColor[3];
// Finally, we can get the final blendFactor, which is 1 - the material factor.
blendFactor = 1f - oneMinusFactor;
// --- Compact method ---
// float blendFactor = this.blendFactor * (1f - ((1f - pixelColor[3]) * materialColor[3]));
// float oneMinusFactor = 1f - blendFactor;
float col;
switch (blendType) {
case MTEX_BLEND:
result[0] = blendFactor * pixelColor[0] + oneMinusFactor * materialColor[0];
result[1] = blendFactor * pixelColor[1] + oneMinusFactor * materialColor[1];
result[2] = blendFactor * pixelColor[2] + oneMinusFactor * materialColor[2];
break;
case MTEX_MUL:
result[0] = (oneMinusFactor + blendFactor * materialColor[0]) * pixelColor[0];
result[1] = (oneMinusFactor + blendFactor * materialColor[1]) * pixelColor[1];
result[2] = (oneMinusFactor + blendFactor * materialColor[2]) * pixelColor[2];
break;
case MTEX_DIV:
if (pixelColor[0] != 0.0) {
result[0] = (oneMinusFactor * materialColor[0] + blendFactor * materialColor[0] / pixelColor[0]) * 0.5f;
}
if (pixelColor[1] != 0.0) {
result[1] = (oneMinusFactor * materialColor[1] + blendFactor * materialColor[1] / pixelColor[1]) * 0.5f;
}
if (pixelColor[2] != 0.0) {
result[2] = (oneMinusFactor * materialColor[2] + blendFactor * materialColor[2] / pixelColor[2]) * 0.5f;
}
break;
case MTEX_SCREEN:
result[0] = 1.0f - (oneMinusFactor + blendFactor * (1.0f - materialColor[0])) * (1.0f - pixelColor[0]);
result[1] = 1.0f - (oneMinusFactor + blendFactor * (1.0f - materialColor[1])) * (1.0f - pixelColor[1]);
result[2] = 1.0f - (oneMinusFactor + blendFactor * (1.0f - materialColor[2])) * (1.0f - pixelColor[2]);
break;
case MTEX_OVERLAY:
if (materialColor[0] < 0.5f) {
result[0] = pixelColor[0] * (oneMinusFactor + 2.0f * blendFactor * materialColor[0]);
} else {
result[0] = 1.0f - (oneMinusFactor + 2.0f * blendFactor * (1.0f - materialColor[0])) * (1.0f - pixelColor[0]);
}
if (materialColor[1] < 0.5f) {
result[1] = pixelColor[1] * (oneMinusFactor + 2.0f * blendFactor * materialColor[1]);
} else {
result[1] = 1.0f - (oneMinusFactor + 2.0f * blendFactor * (1.0f - materialColor[1])) * (1.0f - pixelColor[1]);
}
if (materialColor[2] < 0.5f) {
result[2] = pixelColor[2] * (oneMinusFactor + 2.0f * blendFactor * materialColor[2]);
} else {
result[2] = 1.0f - (oneMinusFactor + 2.0f * blendFactor * (1.0f - materialColor[2])) * (1.0f - pixelColor[2]);
}
break;
case MTEX_SUB:
result[0] = materialColor[0] - blendFactor * pixelColor[0];
result[1] = materialColor[1] - blendFactor * pixelColor[1];
result[2] = materialColor[2] - blendFactor * pixelColor[2];
result[0] = FastMath.clamp(result[0], 0.0f, 1.0f);
result[1] = FastMath.clamp(result[1], 0.0f, 1.0f);
result[2] = FastMath.clamp(result[2], 0.0f, 1.0f);
break;
case MTEX_ADD:
result[0] = (blendFactor * pixelColor[0] + materialColor[0]) * 0.5f;
result[1] = (blendFactor * pixelColor[1] + materialColor[1]) * 0.5f;
result[2] = (blendFactor * pixelColor[2] + materialColor[2]) * 0.5f;
break;
case MTEX_DIFF:
result[0] = oneMinusFactor * materialColor[0] + blendFactor * Math.abs(materialColor[0] - pixelColor[0]);
result[1] = oneMinusFactor * materialColor[1] + blendFactor * Math.abs(materialColor[1] - pixelColor[1]);
result[2] = oneMinusFactor * materialColor[2] + blendFactor * Math.abs(materialColor[2] - pixelColor[2]);
break;
case MTEX_DARK:
col = blendFactor * pixelColor[0];
result[0] = col < materialColor[0] ? col : materialColor[0];
col = blendFactor * pixelColor[1];
result[1] = col < materialColor[1] ? col : materialColor[1];
col = blendFactor * pixelColor[2];
result[2] = col < materialColor[2] ? col : materialColor[2];
break;
case MTEX_LIGHT:
col = blendFactor * pixelColor[0];
result[0] = col > materialColor[0] ? col : materialColor[0];
col = blendFactor * pixelColor[1];
result[1] = col > materialColor[1] ? col : materialColor[1];
col = blendFactor * pixelColor[2];
result[2] = col > materialColor[2] ? col : materialColor[2];
break;
case MTEX_BLEND_HUE:
case MTEX_BLEND_SAT:
case MTEX_BLEND_VAL:
case MTEX_BLEND_COLOR:
System.arraycopy(materialColor, 0, result, 0, 3);
this.blendHSV(blendType, result, blendFactor, pixelColor, blenderContext);
break;
default:
throw new IllegalStateException("Unknown blend type: " + blendType);
}
}
}

@ -1,131 +0,0 @@
package com.jme3.scene.plugins.blender.textures.blending;
import com.jme3.scene.plugins.blender.BlenderContext;
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.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import jme3tools.converters.RGB565;
/**
* <p>
* The class that is responsible for blending the following texture types:
* </p>
* <ul>
* <li>DXT1</li>
* <li>DXT1A</li>
* <li>DXT3</li>
* <li>DXT5</li>
* </ul>
* @author Marcin Roguski (Kaelthas)
*/
public class TextureBlenderDDS extends TextureBlenderAWT {
public TextureBlenderDDS(int flag, boolean negateTexture, int blendType, float[] materialColor, float[] color, float blendFactor) {
super(flag, negateTexture, blendType, materialColor, color, blendFactor);
}
@Override
public Image blend(Image image, Image baseImage, BlenderContext blenderContext) {
this.prepareImagesForBlending(image, baseImage);
Format format = image.getFormat();
int width = image.getWidth();
int height = image.getHeight();
int depth = image.getDepth();
if (depth == 0) {
depth = 1;
}
ArrayList<ByteBuffer> dataArray = new ArrayList<ByteBuffer>(depth);
PixelInputOutput basePixelIO = null;
float[][] compressedMaterialColor = null;
TexturePixel[] baseTextureColors = null;
if (baseImage != null) {
basePixelIO = PixelIOFactory.getPixelIO(baseImage.getFormat());
compressedMaterialColor = new float[2][4];
baseTextureColors = new TexturePixel[] { new TexturePixel(), new TexturePixel() };
}
float[] resultPixel = new float[4];
float[] pixelColor = new float[4];
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel() };
int baseXTexelIndex = 0, baseYTexelIndex = 0;
float[] alphas = new float[] { 1, 1 };
for (int dataLayerIndex = 0; dataLayerIndex < depth; ++dataLayerIndex) {
ByteBuffer data = image.getData(dataLayerIndex);
data.rewind();
ByteBuffer newData = BufferUtils.createByteBuffer(data.remaining());
while (data.hasRemaining()) {
if (format == Format.DXT3) {
long alpha = data.getLong();
// get alpha for first and last pixel that is compressed in the texel
byte alpha0 = (byte) (alpha << 4 & 0xFF);
byte alpha1 = (byte) (alpha >> 60 & 0xFF);
alphas[0] = alpha0 >= 0 ? alpha0 / 255.0f : 1.0f - ~alpha0 / 255.0f;
alphas[1] = alpha1 >= 0 ? alpha1 / 255.0f : 1.0f - ~alpha1 / 255.0f;
newData.putLong(alpha);
} else if (format == Format.DXT5) {
byte alpha0 = data.get();
byte alpha1 = data.get();
alphas[0] = alpha0 >= 0 ? alpha0 / 255.0f : 1.0f - ~alpha0 / 255.0f;
alphas[1] = alpha1 >= 0 ? alpha0 / 255.0f : 1.0f - ~alpha0 / 255.0f;
newData.put(alpha0);
newData.put(alpha1);
// only read the next 6 bytes (these are alpha indexes)
newData.putInt(data.getInt());
newData.putShort(data.getShort());
}
int col0 = RGB565.RGB565_to_ARGB8(data.getShort());
int col1 = RGB565.RGB565_to_ARGB8(data.getShort());
colors[0].fromARGB8(col0);
colors[1].fromARGB8(col1);
// compressing 16 pixels from the base texture as if they belonged to a texel
if (baseImage != null) {
// reading pixels (first and last of the 16 colors array)
basePixelIO.read(baseImage, dataLayerIndex, baseTextureColors[0], baseXTexelIndex << 2, baseYTexelIndex << 2);// first pixel
basePixelIO.read(baseImage, dataLayerIndex, baseTextureColors[1], baseXTexelIndex << 2 + 4, baseYTexelIndex << 2 + 4);// last pixel
baseTextureColors[0].toRGBA(compressedMaterialColor[0]);
baseTextureColors[1].toRGBA(compressedMaterialColor[1]);
}
// blending colors
for (int i = 0; i < colors.length; ++i) {
if (negateTexture) {
colors[i].negate();
}
colors[i].toRGBA(pixelColor);
pixelColor[3] = alphas[i];
this.blendPixel(resultPixel, compressedMaterialColor != null ? compressedMaterialColor[i] : materialColor, pixelColor, blenderContext);
colors[i].fromARGB(1, resultPixel[0], resultPixel[1], resultPixel[2]);
int argb8 = colors[i].toARGB8();
short rgb565 = RGB565.ARGB8_to_RGB565(argb8);
newData.putShort(rgb565);
}
// just copy the remaining 4 bytes of the current texel
newData.putInt(data.getInt());
++baseXTexelIndex;
if (baseXTexelIndex > image.getWidth() >> 2) {
baseXTexelIndex = 0;
++baseYTexelIndex;
}
}
dataArray.add(newData);
}
Image result = dataArray.size() > 1 ? new Image(format, width, height, depth, dataArray, ColorSpace.Linear) : new Image(format, width, height, dataArray.get(0), ColorSpace.Linear);
if (image.getMipMapSizes() != null) {
result.setMipMapSizes(image.getMipMapSizes().clone());
}
return result;
}
}

@ -1,117 +0,0 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures.blending;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
/**
* This class creates the texture blending class depending on the texture type.
*
* @author Marcin Roguski (Kaelthas)
*/
public class TextureBlenderFactory {
private static final Logger LOGGER = Logger.getLogger(TextureBlenderFactory.class.getName());
/**
* A blender that does not change the image. Used for none supported image types.
*/
private static final TextureBlender NON_CHANGING_BLENDER = new TextureBlender() {
@Override
public Image blend(Image image, Image baseImage, BlenderContext blenderContext) {
return image;
}
@Override
public void copyBlendingData(TextureBlender textureBlender) { }
};
/**
* This method creates the blending class.
*
* @param format
* the texture format
* @return texture blending class
*/
public static TextureBlender createTextureBlender(Format format, int flag, boolean negate, int blendType, float[] materialColor, float[] color, float colfac) {
switch (format) {
case Luminance8:
case Luminance8Alpha8:
case Luminance16F:
case Luminance16FAlpha16F:
case Luminance32F:
return new TextureBlenderLuminance(flag, negate, blendType, materialColor, color, colfac);
case RGBA8:
case ABGR8:
case BGR8:
case RGB8:
case RGB111110F:
case RGB16F:
case RGB16F_to_RGB111110F:
case RGB16F_to_RGB9E5:
case RGB32F:
case RGB565:
case RGB5A1:
case RGB9E5:
case RGBA16F:
case RGBA32F:
return new TextureBlenderAWT(flag, negate, blendType, materialColor, color, colfac);
case DXT1:
case DXT1A:
case DXT3:
case DXT5:
return new TextureBlenderDDS(flag, negate, blendType, materialColor, color, colfac);
default:
LOGGER.log(Level.WARNING, "Image type not yet supported for blending: {0}. Returning a blender that does not change the texture.", format);
return NON_CHANGING_BLENDER;
}
}
/**
* This method changes the image format in the texture blender.
*
* @param format
* the new image format
* @param textureBlender
* the texture blender that will be altered
* @return altered texture blender
*/
public static TextureBlender alterTextureType(Format format, TextureBlender textureBlender) {
TextureBlender result = TextureBlenderFactory.createTextureBlender(format, 0, false, 0, null, null, 0);
result.copyBlendingData(textureBlender);
return result;
}
}

@ -1,259 +0,0 @@
package com.jme3.scene.plugins.blender.textures.blending;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.BlenderContext;
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.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>
* The class that is responsible for blending the following texture types:
* </p>
* <ul>
* <li>Luminance8</li>
* <li>Luminance8Alpha8</li>
* </ul>
* <p>
* Not yet supported (but will be):
* </p>
* <ul>
* <li>Luminance16</li>
* <li>Luminance16Alpha16</li>
* <li>Luminance16F</li>
* <li>Luminance16FAlpha16F</li>
* <li>Luminance32F</li>
* </ul>
*
* @author Marcin Roguski (Kaelthas)
*/
public class TextureBlenderLuminance extends AbstractTextureBlender {
private static final Logger LOGGER = Logger.getLogger(TextureBlenderLuminance.class.getName());
public TextureBlenderLuminance(int flag, boolean negateTexture, int blendType, float[] materialColor, float[] color, float blendFactor) {
super(flag, negateTexture, blendType, materialColor, color, blendFactor);
}
@Override
public Image blend(Image image, Image baseImage, BlenderContext blenderContext) {
this.prepareImagesForBlending(image, baseImage);
Format format = image.getFormat();
PixelInputOutput basePixelIO = null;
TexturePixel basePixel = null;
float[] materialColor = this.materialColor;
if (baseImage != null) {
basePixelIO = PixelIOFactory.getPixelIO(baseImage.getFormat());
materialColor = new float[this.materialColor.length];
basePixel = new TexturePixel();
}
int width = image.getWidth();
int height = image.getHeight();
int depth = image.getDepth();
if (depth == 0) {
depth = 1;
}
ArrayList<ByteBuffer> dataArray = new ArrayList<ByteBuffer>(depth);
float[] resultPixel = new float[4];
float[] tinAndAlpha = new float[2];
for (int dataLayerIndex = 0; dataLayerIndex < depth; ++dataLayerIndex) {
ByteBuffer data = image.getData(dataLayerIndex);
data.rewind();
ByteBuffer newData = BufferUtils.createByteBuffer(data.limit() * 4);
int dataIndex = 0, x = 0, y = 0;
while (data.hasRemaining()) {
// getting the proper material color if the base texture is applied
if (basePixelIO != null) {
basePixelIO.read(baseImage, dataLayerIndex, basePixel, x, y);
basePixel.toRGBA(materialColor);
++x;
if (x >= width) {
x = 0;
++y;
}
}
this.getTinAndAlpha(data, format, negateTexture, tinAndAlpha);
this.blendPixel(resultPixel, materialColor, color, tinAndAlpha[0], blendFactor, blendType, blenderContext);
newData.put(dataIndex++, (byte) (resultPixel[0] * 255.0f));
newData.put(dataIndex++, (byte) (resultPixel[1] * 255.0f));
newData.put(dataIndex++, (byte) (resultPixel[2] * 255.0f));
newData.put(dataIndex++, (byte) (tinAndAlpha[1] * 255.0f));
}
dataArray.add(newData);
}
Image result = depth > 1 ? new Image(Format.RGBA8, width, height, depth, dataArray, ColorSpace.Linear) : new Image(Format.RGBA8, width, height, dataArray.get(0), ColorSpace.Linear);
if (image.getMipMapSizes() != null) {
result.setMipMapSizes(image.getMipMapSizes().clone());
}
return result;
}
/**
* This method return texture intensity and alpha value.
*
* @param data
* the texture data
* @param imageFormat
* the image format
* @param neg
* indicates if the texture is negated
* @param result
* the table (2 elements) where the result is being stored
*/
protected void getTinAndAlpha(ByteBuffer data, Format imageFormat, boolean neg, float[] result) {
byte pixelValue = data.get();// at least one byte is always taken
float firstPixelValue = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f;
switch (imageFormat) {
case Luminance8:
result[0] = neg ? 1.0f - firstPixelValue : firstPixelValue;
result[1] = 1.0f;
break;
case Luminance8Alpha8:
result[0] = neg ? 1.0f - firstPixelValue : firstPixelValue;
pixelValue = data.get();
result[1] = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f;
break;
case Luminance16F:
case Luminance16FAlpha16F:
case Luminance32F:
LOGGER.log(Level.WARNING, "Image type not yet supported for blending: {0}", imageFormat);
break;
default:
throw new IllegalStateException("Invalid image format type for Luminance texture blender: " + imageFormat);
}
}
/**
* This method blends the texture with an appropriate color.
*
* @param result
* the result color (variable 'in' in blender source code)
* @param materialColor
* the texture color (variable 'out' in blender source coude)
* @param color
* the previous color (variable 'tex' in blender source code)
* @param textureIntensity
* texture intensity (variable 'fact' in blender source code)
* @param textureFactor
* texture affection factor (variable 'facg' in blender source
* code)
* @param blendtype
* the blend type
* @param blenderContext
* the blender context
*/
protected void blendPixel(float[] result, float[] materialColor, float[] color, float textureIntensity, float textureFactor, int blendtype, BlenderContext blenderContext) {
float oneMinusFactor, col;
textureIntensity *= textureFactor;
switch (blendtype) {
case MTEX_BLEND:
oneMinusFactor = 1.0f - textureIntensity;
result[0] = textureIntensity * color[0] + oneMinusFactor * materialColor[0];
result[1] = textureIntensity * color[1] + oneMinusFactor * materialColor[1];
result[2] = textureIntensity * color[2] + oneMinusFactor * materialColor[2];
break;
case MTEX_MUL:
oneMinusFactor = 1.0f - textureFactor;
result[0] = (oneMinusFactor + textureIntensity * materialColor[0]) * color[0];
result[1] = (oneMinusFactor + textureIntensity * materialColor[1]) * color[1];
result[2] = (oneMinusFactor + textureIntensity * materialColor[2]) * color[2];
break;
case MTEX_DIV:
oneMinusFactor = 1.0f - textureIntensity;
if (color[0] != 0.0) {
result[0] = (oneMinusFactor * materialColor[0] + textureIntensity * materialColor[0] / color[0]) * 0.5f;
}
if (color[1] != 0.0) {
result[1] = (oneMinusFactor * materialColor[1] + textureIntensity * materialColor[1] / color[1]) * 0.5f;
}
if (color[2] != 0.0) {
result[2] = (oneMinusFactor * materialColor[2] + textureIntensity * materialColor[2] / color[2]) * 0.5f;
}
break;
case MTEX_SCREEN:
oneMinusFactor = 1.0f - textureFactor;
result[0] = 1.0f - (oneMinusFactor + textureIntensity * (1.0f - materialColor[0])) * (1.0f - color[0]);
result[1] = 1.0f - (oneMinusFactor + textureIntensity * (1.0f - materialColor[1])) * (1.0f - color[1]);
result[2] = 1.0f - (oneMinusFactor + textureIntensity * (1.0f - materialColor[2])) * (1.0f - color[2]);
break;
case MTEX_OVERLAY:
oneMinusFactor = 1.0f - textureFactor;
if (materialColor[0] < 0.5f) {
result[0] = color[0] * (oneMinusFactor + 2.0f * textureIntensity * materialColor[0]);
} else {
result[0] = 1.0f - (oneMinusFactor + 2.0f * textureIntensity * (1.0f - materialColor[0])) * (1.0f - color[0]);
}
if (materialColor[1] < 0.5f) {
result[1] = color[1] * (oneMinusFactor + 2.0f * textureIntensity * materialColor[1]);
} else {
result[1] = 1.0f - (oneMinusFactor + 2.0f * textureIntensity * (1.0f - materialColor[1])) * (1.0f - color[1]);
}
if (materialColor[2] < 0.5f) {
result[2] = color[2] * (oneMinusFactor + 2.0f * textureIntensity * materialColor[2]);
} else {
result[2] = 1.0f - (oneMinusFactor + 2.0f * textureIntensity * (1.0f - materialColor[2])) * (1.0f - color[2]);
}
break;
case MTEX_SUB:
result[0] = materialColor[0] - textureIntensity * color[0];
result[1] = materialColor[1] - textureIntensity * color[1];
result[2] = materialColor[2] - textureIntensity * color[2];
result[0] = FastMath.clamp(result[0], 0.0f, 1.0f);
result[1] = FastMath.clamp(result[1], 0.0f, 1.0f);
result[2] = FastMath.clamp(result[2], 0.0f, 1.0f);
break;
case MTEX_ADD:
result[0] = (textureIntensity * color[0] + materialColor[0]) * 0.5f;
result[1] = (textureIntensity * color[1] + materialColor[1]) * 0.5f;
result[2] = (textureIntensity * color[2] + materialColor[2]) * 0.5f;
break;
case MTEX_DIFF:
oneMinusFactor = 1.0f - textureIntensity;
result[0] = oneMinusFactor * materialColor[0] + textureIntensity * Math.abs(materialColor[0] - color[0]);
result[1] = oneMinusFactor * materialColor[1] + textureIntensity * Math.abs(materialColor[1] - color[1]);
result[2] = oneMinusFactor * materialColor[2] + textureIntensity * Math.abs(materialColor[2] - color[2]);
break;
case MTEX_DARK:
col = textureIntensity * color[0];
result[0] = col < materialColor[0] ? col : materialColor[0];
col = textureIntensity * color[1];
result[1] = col < materialColor[1] ? col : materialColor[1];
col = textureIntensity * color[2];
result[2] = col < materialColor[2] ? col : materialColor[2];
break;
case MTEX_LIGHT:
col = textureIntensity * color[0];
result[0] = col > materialColor[0] ? col : materialColor[0];
col = textureIntensity * color[1];
result[1] = col > materialColor[1] ? col : materialColor[1];
col = textureIntensity * color[2];
result[2] = col > materialColor[2] ? col : materialColor[2];
break;
case MTEX_BLEND_HUE:
case MTEX_BLEND_SAT:
case MTEX_BLEND_VAL:
case MTEX_BLEND_COLOR:
System.arraycopy(materialColor, 0, result, 0, 3);
this.blendHSV(blendtype, result, textureIntensity, color, blenderContext);
break;
default:
throw new IllegalStateException("Unknown blend type: " + blendtype);
}
}
}

@ -1,814 +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.
*/
package com.jme3.scene.plugins.blender.textures.generating;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.textures.generating.TextureGeneratorMusgrave.MusgraveData;
/**
* This generator is responsible for creating various noises used to create
* generated textures loaded from blender.
* It is only used by TextureHelper.
* Take note that these functions are not thread safe.
* @author Marcin Roguski (Kaelthas)
*/
/* package */class NoiseGenerator {
private static final Logger LOGGER = Logger.getLogger(NoiseGenerator.class.getName());
// tex->stype
protected static final int TEX_PLASTIC = 0;
protected static final int TEX_WALLIN = 1;
protected static final int TEX_WALLOUT = 2;
// musgrave stype
protected static final int TEX_MFRACTAL = 0;
protected static final int TEX_RIDGEDMF = 1;
protected static final int TEX_HYBRIDMF = 2;
protected static final int TEX_FBM = 3;
protected static final int TEX_HTERRAIN = 4;
// keyblock->type
protected static final int KEY_LINEAR = 0;
protected static final int KEY_CARDINAL = 1;
protected static final int KEY_BSPLINE = 2;
// CONSTANTS (read from file)
protected static float[] hashpntf;
protected static short[] hash;
protected static float[] hashvectf;
protected static short[] p;
protected static float[][] g;
/**
* Constructor. Loads the constants needed for computations. They are exactly like the ones the blender uses. Each
* deriving class should override this method and load its own constraints.
*/
public NoiseGenerator() {
LOGGER.fine("Loading noise constants.");
InputStream is = NoiseGenerator.class.getResourceAsStream("noiseconstants.dat");
try {
ObjectInputStream ois = new ObjectInputStream(is);
hashpntf = (float[]) ois.readObject();
hash = (short[]) ois.readObject();
hashvectf = (float[]) ois.readObject();
p = (short[]) ois.readObject();
g = (float[][]) ois.readObject();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
} catch (ClassNotFoundException e) {
assert false : "Constants' classes should be arrays of primitive types, so they are ALWAYS known!";
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, e.getLocalizedMessage());
}
}
}
}
protected static Map<Integer, NoiseFunction> noiseFunctions = new HashMap<Integer, NoiseFunction>();
static {
noiseFunctions.put(Integer.valueOf(0), new NoiseFunction() {
// originalBlenderNoise
@Override
public float execute(float x, float y, float z) {
return NoiseFunctions.originalBlenderNoise(x, y, z);
}
@Override
public float executeSigned(float x, float y, float z) {
return 2.0f * NoiseFunctions.originalBlenderNoise(x, y, z) - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(1), new NoiseFunction() {
// orgPerlinNoise
@Override
public float execute(float x, float y, float z) {
return 0.5f + 0.5f * NoiseFunctions.noise3Perlin(x, y, z);
}
@Override
public float executeSigned(float x, float y, float z) {
return NoiseFunctions.noise3Perlin(x, y, z);
}
});
noiseFunctions.put(Integer.valueOf(2), new NoiseFunction() {
// newPerlin
@Override
public float execute(float x, float y, float z) {
return 0.5f + 0.5f * NoiseFunctions.newPerlin(x, y, z);
}
@Override
public float executeSigned(float x, float y, float z) {
return this.execute(x, y, z);
}
});
noiseFunctions.put(Integer.valueOf(3), new NoiseFunction() {
private final float[] da = new float[4];
private final float[] pa = new float[12];
// voronoi_F1
@Override
public float execute(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return da[0];
}
@Override
public float executeSigned(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return 2.0f * da[0] - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(4), new NoiseFunction() {
private final float[] da = new float[4];
private final float[] pa = new float[12];
// voronoi_F2
@Override
public float execute(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return da[1];
}
@Override
public float executeSigned(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return 2.0f * da[1] - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(5), new NoiseFunction() {
private final float[] da = new float[4];
private final float[] pa = new float[12];
// voronoi_F3
@Override
public float execute(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return da[2];
}
@Override
public float executeSigned(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return 2.0f * da[2] - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(6), new NoiseFunction() {
private final float[] da = new float[4];
private final float[] pa = new float[12];
// voronoi_F4
@Override
public float execute(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return da[3];
}
@Override
public float executeSigned(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return 2.0f * da[3] - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(7), new NoiseFunction() {
private final float[] da = new float[4];
private final float[] pa = new float[12];
// voronoi_F1F2
@Override
public float execute(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return da[1] - da[0];
}
@Override
public float executeSigned(float x, float y, float z) {
NoiseFunctions.voronoi(x, y, z, da, pa, 1, NATURAL_DISTANCE_FUNCTION);
return 2.0f * (da[1] - da[0]) - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(8), new NoiseFunction() {
private final NoiseFunction voronoiF1F2NoiseFunction = noiseFunctions.get(Integer.valueOf(7));
// voronoi_Cr
@Override
public float execute(float x, float y, float z) {
float t = 10 * voronoiF1F2NoiseFunction.execute(x, y, z);
return t > 1.0f ? 1.0f : t;
}
@Override
public float executeSigned(float x, float y, float z) {
float t = 10.0f * voronoiF1F2NoiseFunction.execute(x, y, z);
return t > 1.0f ? 1.0f : 2.0f * t - 1.0f;
}
});
noiseFunctions.put(Integer.valueOf(14), new NoiseFunction() {
// cellNoise
@Override
public float execute(float x, float y, float z) {
int xi = (int) FastMath.floor(x);
int yi = (int) FastMath.floor(y);
int zi = (int) FastMath.floor(z);
long n = xi + yi * 1301 + zi * 314159;
n ^= n << 13;
return (n * (n * n * 15731 + 789221) + 1376312589) / 4294967296.0f;
}
@Override
public float executeSigned(float x, float y, float z) {
return 2.0f * this.execute(x, y, z) - 1.0f;
}
});
}
/** Distance metrics for voronoi. e parameter only used in Minkovsky. */
protected static Map<Integer, DistanceFunction> distanceFunctions = new HashMap<Integer, NoiseGenerator.DistanceFunction>();
private static DistanceFunction NATURAL_DISTANCE_FUNCTION; // often used in noise functions, se I store it here to avoid fetching it every time
static {
distanceFunctions.put(Integer.valueOf(0), new DistanceFunction() {
// real distance
@Override
public float execute(float x, float y, float z, float e) {
return (float) Math.sqrt(x * x + y * y + z * z);
}
});
distanceFunctions.put(Integer.valueOf(1), new DistanceFunction() {
// distance squared
@Override
public float execute(float x, float y, float z, float e) {
return x * x + y * y + z * z;
}
});
distanceFunctions.put(Integer.valueOf(2), new DistanceFunction() {
// manhattan/taxicab/cityblock distance
@Override
public float execute(float x, float y, float z, float e) {
return FastMath.abs(x) + FastMath.abs(y) + FastMath.abs(z);
}
});
distanceFunctions.put(Integer.valueOf(3), new DistanceFunction() {
// Chebychev
@Override
public float execute(float x, float y, float z, float e) {
x = FastMath.abs(x);
y = FastMath.abs(y);
z = FastMath.abs(z);
float t = x > y ? x : y;
return z > t ? z : t;
}
});
distanceFunctions.put(Integer.valueOf(4), new DistanceFunction() {
// Minkovsky, preset exponent 0.5 (MinkovskyH)
@Override
public float execute(float x, float y, float z, float e) {
float d = (float) (Math.sqrt(FastMath.abs(x)) + Math.sqrt(FastMath.abs(y)) + Math.sqrt(FastMath.abs(z)));
return d * d;
}
});
distanceFunctions.put(Integer.valueOf(5), new DistanceFunction() {
// Minkovsky, preset exponent 0.25 (Minkovsky4)
@Override
public float execute(float x, float y, float z, float e) {
x *= x;
y *= y;
z *= z;
return (float) Math.sqrt(Math.sqrt(x * x + y * y + z * z));
}
});
distanceFunctions.put(Integer.valueOf(6), new DistanceFunction() {
// Minkovsky, general case
@Override
public float execute(float x, float y, float z, float e) {
return (float) Math.pow(Math.pow(FastMath.abs(x), e) + Math.pow(FastMath.abs(y), e) + Math.pow(FastMath.abs(z), e), 1.0f / e);
}
});
NATURAL_DISTANCE_FUNCTION = distanceFunctions.get(Integer.valueOf(0));
}
protected static Map<Integer, MusgraveFunction> musgraveFunctions = new HashMap<Integer, NoiseGenerator.MusgraveFunction>();
static {
musgraveFunctions.put(Integer.valueOf(TEX_MFRACTAL), new MusgraveFunction() {
@Override
public float execute(MusgraveData musgraveData, float x, float y, float z) {
float rmd, value = 1.0f, pwr = 1.0f, pwHL = (float) Math.pow(musgraveData.lacunarity, -musgraveData.h);
for (int i = 0; i < (int) musgraveData.octaves; ++i) {
value *= pwr * musgraveData.noiseFunction.executeSigned(x, y, z) + 1.0f;
pwr *= pwHL;
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
}
rmd = musgraveData.octaves - FastMath.floor(musgraveData.octaves);
if (rmd != 0.0f) {
value *= rmd * musgraveData.noiseFunction.executeSigned(x, y, z) * pwr + 1.0f;
}
return value;
}
});
musgraveFunctions.put(Integer.valueOf(TEX_RIDGEDMF), new MusgraveFunction() {
@Override
public float execute(MusgraveData musgraveData, float x, float y, float z) {
float result, signal, weight;
float pwHL = (float) Math.pow(musgraveData.lacunarity, -musgraveData.h);
float pwr = pwHL;
signal = musgraveData.offset - FastMath.abs(musgraveData.noiseFunction.executeSigned(x, y, z));
signal *= signal;
result = signal;
weight = 1.0f;
for (int i = 1; i < (int) musgraveData.octaves; ++i) {
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
weight = signal * musgraveData.gain;
if (weight > 1.0f) {
weight = 1.0f;
} else if (weight < 0.0) {
weight = 0.0f;
}
signal = musgraveData.offset - FastMath.abs(musgraveData.noiseFunction.executeSigned(x, y, z));
signal *= signal;
signal *= weight;
result += signal * pwr;
pwr *= pwHL;
}
return result;
}
});
musgraveFunctions.put(Integer.valueOf(TEX_HYBRIDMF), new MusgraveFunction() {
@Override
public float execute(MusgraveData musgraveData, float x, float y, float z) {
float result, signal, weight, rmd;
float pwHL = (float) Math.pow(musgraveData.lacunarity, -musgraveData.h);
float pwr = pwHL;
result = musgraveData.noiseFunction.executeSigned(x, y, z) + musgraveData.offset;
weight = musgraveData.gain * result;
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
for (int i = 1; weight > 0.001f && i < (int) musgraveData.octaves; ++i) {
if (weight > 1.0f) {
weight = 1.0f;
}
signal = (musgraveData.noiseFunction.executeSigned(x, y, z) + musgraveData.offset) * pwr;
pwr *= pwHL;
result += weight * signal;
weight *= musgraveData.gain * signal;
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
}
rmd = musgraveData.octaves - FastMath.floor(musgraveData.octaves);
if (rmd != 0.0f) {
result += rmd * (musgraveData.noiseFunction.executeSigned(x, y, z) + musgraveData.offset) * pwr;
}
return result;
}
});
musgraveFunctions.put(Integer.valueOf(TEX_FBM), new MusgraveFunction() {
@Override
public float execute(MusgraveData musgraveData, float x, float y, float z) {
float rmd, value = 0.0f, pwr = 1.0f, pwHL = (float) Math.pow(musgraveData.lacunarity, -musgraveData.h);
for (int i = 0; i < (int) musgraveData.octaves; ++i) {
value += musgraveData.noiseFunction.executeSigned(x, y, z) * pwr;
pwr *= pwHL;
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
}
rmd = musgraveData.octaves - FastMath.floor(musgraveData.octaves);
if (rmd != 0.f) {
value += rmd * musgraveData.noiseFunction.executeSigned(x, y, z) * pwr;
}
return value;
}
});
musgraveFunctions.put(Integer.valueOf(TEX_HTERRAIN), new MusgraveFunction() {
@Override
public float execute(MusgraveData musgraveData, float x, float y, float z) {
float value, increment, rmd;
float pwHL = (float) Math.pow(musgraveData.lacunarity, -musgraveData.h);
float pwr = pwHL;
value = musgraveData.offset + musgraveData.noiseFunction.executeSigned(x, y, z);
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
for (int i = 1; i < (int) musgraveData.octaves; ++i) {
increment = (musgraveData.noiseFunction.executeSigned(x, y, z) + musgraveData.offset) * pwr * value;
value += increment;
pwr *= pwHL;
x *= musgraveData.lacunarity;
y *= musgraveData.lacunarity;
z *= musgraveData.lacunarity;
}
rmd = musgraveData.octaves - FastMath.floor(musgraveData.octaves);
if (rmd != 0.0) {
increment = (musgraveData.noiseFunction.executeSigned(x, y, z) + musgraveData.offset) * pwr * value;
value += rmd * increment;
}
return value;
}
});
}
public static class NoiseFunctions {
public static float noise(float x, float y, float z, float noiseSize, int noiseDepth, NoiseFunction noiseFunction, boolean isHard) {
if (noiseSize != 0.0) {
noiseSize = 1.0f / noiseSize;
x *= noiseSize;
y *= noiseSize;
z *= noiseSize;
}
float result = noiseFunction.execute(x, y, z);
return isHard ? FastMath.abs(2.0f * result - 1.0f) : result;
}
public static float turbulence(float x, float y, float z, float noiseSize, int noiseDepth, NoiseFunction noiseFunction, boolean isHard) {
if (noiseSize != 0.0) {
noiseSize = 1.0f / noiseSize;
x *= noiseSize;
y *= noiseSize;
z *= noiseSize;
}
float sum = 0, t, amp = 1, fscale = 1;
for (int i = 0; i <= noiseDepth; ++i, amp *= 0.5f, fscale *= 2f) {
t = noiseFunction.execute(fscale * x, fscale * y, fscale * z);
if (isHard) {
t = FastMath.abs(2.0f * t - 1.0f);
}
sum += t * amp;
}
sum *= (1 << noiseDepth) / (float) ((1 << noiseDepth + 1) - 1);
return sum;
}
private static final float[] voronoiP = new float[3];
public static void voronoi(float x, float y, float z, float[] da, float[] pa, float distanceExponent, DistanceFunction distanceFunction) {
float xd, yd, zd, d;
int xi = (int) FastMath.floor(x);
int yi = (int) FastMath.floor(y);
int zi = (int) FastMath.floor(z);
da[0] = da[1] = da[2] = da[3] = Float.MAX_VALUE;// 1e10f;
for (int i = xi - 1; i <= xi + 1; ++i) {
for (int j = yi - 1; j <= yi + 1; ++j) {
for (int k = zi - 1; k <= zi + 1; ++k) {
NoiseMath.hash(i, j, k, voronoiP);
xd = x - (voronoiP[0] + i);
yd = y - (voronoiP[1] + j);
zd = z - (voronoiP[2] + k);
d = distanceFunction.execute(xd, yd, zd, distanceExponent);
if (d < da[0]) {
da[3] = da[2];
da[2] = da[1];
da[1] = da[0];
da[0] = d;
pa[9] = pa[6];
pa[10] = pa[7];
pa[11] = pa[8];
pa[6] = pa[3];
pa[7] = pa[4];
pa[8] = pa[5];
pa[3] = pa[0];
pa[4] = pa[1];
pa[5] = pa[2];
pa[0] = voronoiP[0] + i;
pa[1] = voronoiP[1] + j;
pa[2] = voronoiP[2] + k;
} else if (d < da[1]) {
da[3] = da[2];
da[2] = da[1];
da[1] = d;
pa[9] = pa[6];
pa[10] = pa[7];
pa[11] = pa[8];
pa[6] = pa[3];
pa[7] = pa[4];
pa[8] = pa[5];
pa[3] = voronoiP[0] + i;
pa[4] = voronoiP[1] + j;
pa[5] = voronoiP[2] + k;
} else if (d < da[2]) {
da[3] = da[2];
da[2] = d;
pa[9] = pa[6];
pa[10] = pa[7];
pa[11] = pa[8];
pa[6] = voronoiP[0] + i;
pa[7] = voronoiP[1] + j;
pa[8] = voronoiP[2] + k;
} else if (d < da[3]) {
da[3] = d;
pa[9] = voronoiP[0] + i;
pa[10] = voronoiP[1] + j;
pa[11] = voronoiP[2] + k;
}
}
}
}
}
// instead of adding another permutation array, just use hash table defined above
public static float newPerlin(float x, float y, float z) {
int A, AA, AB, B, BA, BB;
float floorX = FastMath.floor(x), floorY = FastMath.floor(y), floorZ = FastMath.floor(z);
int intX = (int) floorX & 0xFF, intY = (int) floorY & 0xFF, intZ = (int) floorZ & 0xFF;
x -= floorX;
y -= floorY;
z -= floorZ;
// computing fading curves
floorX = NoiseMath.npfade(x);
floorY = NoiseMath.npfade(y);
floorZ = NoiseMath.npfade(z);
A = hash[intX] + intY;
AA = hash[A] + intZ;
AB = hash[A + 1] + intZ;
B = hash[intX + 1] + intY;
BA = hash[B] + intZ;
BB = hash[B + 1] + intZ;
return NoiseMath.lerp(floorZ, NoiseMath.lerp(floorY, NoiseMath.lerp(floorX, NoiseMath.grad(hash[AA], x, y, z), NoiseMath.grad(hash[BA], x - 1, y, z)), NoiseMath.lerp(floorX, NoiseMath.grad(hash[AB], x, y - 1, z), NoiseMath.grad(hash[BB], x - 1, y - 1, z))),
NoiseMath.lerp(floorY, NoiseMath.lerp(floorX, NoiseMath.grad(hash[AA + 1], x, y, z - 1), NoiseMath.grad(hash[BA + 1], x - 1, y, z - 1)), NoiseMath.lerp(floorX, NoiseMath.grad(hash[AB + 1], x, y - 1, z - 1), NoiseMath.grad(hash[BB + 1], x - 1, y - 1, z - 1))));
}
public static float noise3Perlin(float x, float y, float z) {
float t = x + 10000.0f;
int bx0 = (int) t & 0xFF;
int bx1 = bx0 + 1 & 0xFF;
float rx0 = t - (int) t;
float rx1 = rx0 - 1.0f;
t = y + 10000.0f;
int by0 = (int) t & 0xFF;
int by1 = by0 + 1 & 0xFF;
float ry0 = t - (int) t;
float ry1 = ry0 - 1.0f;
t = z + 10000.0f;
int bz0 = (int) t & 0xFF;
int bz1 = bz0 + 1 & 0xFF;
float rz0 = t - (int) t;
float rz1 = rz0 - 1.0f;
int i = p[bx0];
int j = p[bx1];
int b00 = p[i + by0];
int b10 = p[j + by0];
int b01 = p[i + by1];
int b11 = p[j + by1];
float sx = NoiseMath.surve(rx0);
float sy = NoiseMath.surve(ry0);
float sz = NoiseMath.surve(rz0);
float[] q = g[b00 + bz0];
float u = NoiseMath.at(rx0, ry0, rz0, q);
q = g[b10 + bz0];
float v = NoiseMath.at(rx1, ry0, rz0, q);
float a = NoiseMath.lerp(sx, u, v);
q = g[b01 + bz0];
u = NoiseMath.at(rx0, ry1, rz0, q);
q = g[b11 + bz0];
v = NoiseMath.at(rx1, ry1, rz0, q);
float b = NoiseMath.lerp(sx, u, v);
float c = NoiseMath.lerp(sy, a, b);
q = g[b00 + bz1];
u = NoiseMath.at(rx0, ry0, rz1, q);
q = g[b10 + bz1];
v = NoiseMath.at(rx1, ry0, rz1, q);
a = NoiseMath.lerp(sx, u, v);
q = g[b01 + bz1];
u = NoiseMath.at(rx0, ry1, rz1, q);
q = g[b11 + bz1];
v = NoiseMath.at(rx1, ry1, rz1, q);
b = NoiseMath.lerp(sx, u, v);
float d = NoiseMath.lerp(sy, a, b);
return 1.5f * NoiseMath.lerp(sz, c, d);
}
private static final float[] cn = new float[8];
private static final int[] b1 = new int[8];
private static final int[] b2 = new int[2];
private static final float[] xFactor = new float[8];
private static final float[] yFactor = new float[8];
private static final float[] zFactor = new float[8];
public static float originalBlenderNoise(float x, float y, float z) {
float n = 0.5f;
int ix = (int) FastMath.floor(x);
int iy = (int) FastMath.floor(y);
int iz = (int) FastMath.floor(z);
float ox = x - ix;
float oy = y - iy;
float oz = z - iz;
float jx = ox - 1;
float jy = oy - 1;
float jz = oz - 1;
float cn1 = ox * ox;
float cn2 = oy * oy;
float cn3 = oz * oz;
float cn4 = jx * jx;
float cn5 = jy * jy;
float cn6 = jz * jz;
cn1 = 1.0f - 3.0f * cn1 + 2.0f * cn1 * ox;
cn2 = 1.0f - 3.0f * cn2 + 2.0f * cn2 * oy;
cn3 = 1.0f - 3.0f * cn3 + 2.0f * cn3 * oz;
cn4 = 1.0f - 3.0f * cn4 - 2.0f * cn4 * jx;
cn5 = 1.0f - 3.0f * cn5 - 2.0f * cn5 * jy;
cn6 = 1.0f - 3.0f * cn6 - 2.0f * cn6 * jz;
cn[0] = cn1 * cn2 * cn3;
cn[1] = cn1 * cn2 * cn6;
cn[2] = cn1 * cn5 * cn3;
cn[3] = cn1 * cn5 * cn6;
cn[4] = cn4 * cn2 * cn3;
cn[5] = cn4 * cn2 * cn6;
cn[6] = cn4 * cn5 * cn3;
cn[7] = cn4 * cn5 * cn6;
b1[0] = b1[1] = hash[hash[ix & 0xFF] + (iy & 0xFF)];
b1[2] = b1[3] = hash[hash[ix & 0xFF] + (iy + 1 & 0xFF)];
b1[4] = b1[5] = hash[hash[ix + 1 & 0xFF] + (iy & 0xFF)];
b1[6] = b1[7] = hash[hash[ix + 1 & 0xFF] + (iy + 1 & 0xFF)];
b2[0] = iz & 0xFF;
b2[1] = iz + 1 & 0xFF;
xFactor[0] = xFactor[1] = xFactor[2] = xFactor[3] = ox;
xFactor[4] = xFactor[5] = xFactor[6] = xFactor[7] = jx;
yFactor[0] = yFactor[1] = yFactor[4] = yFactor[5] = oy;
yFactor[2] = yFactor[3] = yFactor[6] = yFactor[7] = jy;
zFactor[0] = zFactor[2] = zFactor[4] = zFactor[6] = oz;
zFactor[1] = zFactor[3] = zFactor[5] = zFactor[7] = jz;
for (int i = 0; i < cn.length; ++i) {
int hIndex = 3 * hash[b1[i] + b2[i % 2]];
n += cn[i] * (hashvectf[hIndex] * xFactor[i] + hashvectf[hIndex + 1] * yFactor[i] + hashvectf[hIndex + 2] * zFactor[i]);
}
if (n < 0.0f) {
n = 0.0f;
} else if (n > 1.0f) {
n = 1.0f;
}
return n;
}
}
/**
* This class is abstract to the noise functions computations. It has two methods. One calculates the Signed (with
* 'S' at the end) and the other Unsigned value.
* @author Marcin Roguski (Kaelthas)
*/
interface NoiseFunction {
/**
* This method calculates the unsigned value of the noise.
* @param x
* the x texture coordinate
* @param y
* the y texture coordinate
* @param z
* the z texture coordinate
* @return value of the noise
*/
float execute(float x, float y, float z);
/**
* This method calculates the signed value of the noise.
* @param x
* the x texture coordinate
* @param y
* the y texture coordinate
* @param z
* the z texture coordinate
* @return value of the noise
*/
float executeSigned(float x, float y, float z);
}
public static class NoiseMath {
public static float lerp(float t, float a, float b) {
return a + t * (b - a);
}
public static float npfade(float t) {
return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);
}
public static float grad(int hash, float x, float y, float z) {
int h = hash & 0x0F;
float u = h < 8 ? x : y, v = h < 4 ? y : h == 12 || h == 14 ? x : z;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
public static float surve(float t) {
return t * t * (3.0f - 2.0f * t);
}
public static float at(float x, float y, float z, float[] q) {
return x * q[0] + y * q[1] + z * q[2];
}
public static void hash(int x, int y, int z, float[] result) {
result[0] = hashpntf[3 * hash[hash[hash[z & 0xFF] + y & 0xFF] + x & 0xFF]];
result[1] = hashpntf[3 * hash[hash[hash[z & 0xFF] + y & 0xFF] + x & 0xFF] + 1];
result[2] = hashpntf[3 * hash[hash[hash[z & 0xFF] + y & 0xFF] + x & 0xFF] + 2];
}
}
/**
* This interface is used for distance calculation classes. Distance metrics for voronoi. e parameter only used in
* Minkovsky.
*/
interface DistanceFunction {
/**
* This method calculates the distance for voronoi algorithms.
* @param x
* the x coordinate
* @param y
* the y coordinate
* @param z
* the z coordinate
* @param e
* this parameter used in Monkovsky (no idea what it really is ;)
* @return
*/
float execute(float x, float y, float z, float e);
}
interface MusgraveFunction {
float execute(MusgraveData musgraveData, float x, float y, float z);
}
}

@ -1,128 +0,0 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures.generating;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.textures.ColorBand;
import com.jme3.scene.plugins.blender.textures.TexturePixel;
import com.jme3.texture.Image.Format;
/**
* This class is a base class for texture generators.
* @author Marcin Roguski (Kaelthas)
*/
public abstract class TextureGenerator {
protected NoiseGenerator noiseGenerator;
protected int flag;
protected float[][] colorBand;
protected BrightnessAndContrastData bacd;
protected Format imageFormat;
public TextureGenerator(NoiseGenerator noiseGenerator, Format imageFormat) {
this.noiseGenerator = noiseGenerator;
this.imageFormat = imageFormat;
}
public Format getImageFormat() {
return imageFormat;
}
public void readData(Structure tex, BlenderContext blenderContext) {
flag = ((Number) tex.getFieldValue("flag")).intValue();
colorBand = new ColorBand(tex, blenderContext).computeValues();
bacd = new BrightnessAndContrastData(tex);
if (colorBand != null) {
imageFormat = Format.RGBA8;
}
}
public abstract void getPixel(TexturePixel pixel, float x, float y, float z);
/**
* This method applies brightness and contrast for RGB textures.
*/
protected void applyBrightnessAndContrast(BrightnessAndContrastData bacd, TexturePixel texres) {
texres.red = (texres.red - 0.5f) * bacd.contrast + bacd.brightness;
if (texres.red < 0.0f) {
texres.red = 0.0f;
}
texres.green = (texres.green - 0.5f) * bacd.contrast + bacd.brightness;
if (texres.green < 0.0f) {
texres.green = 0.0f;
}
texres.blue = (texres.blue - 0.5f) * bacd.contrast + bacd.brightness;
if (texres.blue < 0.0f) {
texres.blue = 0.0f;
}
}
/**
* This method applies brightness and contrast for Luminance textures.
* @param texres
* @param contrast
* @param brightness
*/
protected void applyBrightnessAndContrast(TexturePixel texres, float contrast, float brightness) {
texres.intensity = (texres.intensity - 0.5f) * contrast + brightness;
if (texres.intensity < 0.0f) {
texres.intensity = 0.0f;
} else if (texres.intensity > 1.0f) {
texres.intensity = 1.0f;
}
}
/**
* This class contains brightness and contrast data.
* @author Marcin Roguski (Kaelthas)
*/
protected static class BrightnessAndContrastData {
public final float contrast;
public final float brightness;
public final float rFactor;
public final float gFactor;
public final float bFactor;
/**
* Constructor reads the required data from the given structure.
* @param tex
* texture structure
*/
public BrightnessAndContrastData(Structure tex) {
contrast = ((Number) tex.getFieldValue("contrast")).floatValue();
brightness = ((Number) tex.getFieldValue("bright")).floatValue() - 0.5f;
rFactor = ((Number) tex.getFieldValue("rfac")).floatValue();
gFactor = ((Number) tex.getFieldValue("gfac")).floatValue();
bFactor = ((Number) tex.getFieldValue("bfac")).floatValue();
}
}
}

@ -1,138 +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.
*/
package com.jme3.scene.plugins.blender.textures.generating;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.textures.TexturePixel;
import com.jme3.texture.Image.Format;
/**
* This class generates the 'blend' texture.
* @author Marcin Roguski (Kaelthas)
*/
public final class TextureGeneratorBlend extends TextureGenerator {
private static final IntensityFunction INTENSITY_FUNCTION[] = new IntensityFunction[7];
static {
INTENSITY_FUNCTION[0] = new IntensityFunction() {// Linear: stype = 0 (TEX_LIN)
@Override
public float getIntensity(float x, float y, float z) {
return (1.0f + x) * 0.5f;
}
};
INTENSITY_FUNCTION[1] = new IntensityFunction() {// Quad: stype = 1 (TEX_QUAD)
@Override
public float getIntensity(float x, float y, float z) {
float result = (1.0f + x) * 0.5f;
return result * result;
}
};
INTENSITY_FUNCTION[2] = new IntensityFunction() {// Ease: stype = 2 (TEX_EASE)
@Override
public float getIntensity(float x, float y, float z) {
float result = (1.0f + x) * 0.5f;
if (result <= 0.0f) {
return 0.0f;
} else if (result >= 1.0f) {
return 1.0f;
} else {
return result * result * (3.0f - 2.0f * result);
}
}
};
INTENSITY_FUNCTION[3] = new IntensityFunction() {// Diagonal: stype = 3 (TEX_DIAG)
@Override
public float getIntensity(float x, float y, float z) {
return (2.0f + x + y) * 0.25f;
}
};
INTENSITY_FUNCTION[4] = new IntensityFunction() {// Sphere: stype = 4 (TEX_SPHERE)
@Override
public float getIntensity(float x, float y, float z) {
float result = 1.0f - (float) Math.sqrt(x * x + y * y + z * z);
return result < 0.0f ? 0.0f : result;
}
};
INTENSITY_FUNCTION[5] = new IntensityFunction() {// Halo: stype = 5 (TEX_HALO)
@Override
public float getIntensity(float x, float y, float z) {
float result = 1.0f - (float) Math.sqrt(x * x + y * y + z * z);
return result <= 0.0f ? 0.0f : result * result;
}
};
INTENSITY_FUNCTION[6] = new IntensityFunction() {// Radial: stype = 6 (TEX_RAD)
@Override
public float getIntensity(float x, float y, float z) {
return (float) Math.atan2(y, x) * FastMath.INV_TWO_PI + 0.5f;
}
};
}
/**
* Constructor stores the given noise generator.
* @param noiseGenerator
* the noise generator
*/
public TextureGeneratorBlend(NoiseGenerator noiseGenerator) {
super(noiseGenerator, Format.Luminance8);
}
protected int stype;
@Override
public void readData(Structure tex, BlenderContext blenderContext) {
super.readData(tex, blenderContext);
stype = ((Number) tex.getFieldValue("stype")).intValue();
}
@Override
public void getPixel(TexturePixel pixel, float x, float y, float z) {
pixel.intensity = INTENSITY_FUNCTION[stype].getIntensity(x, y, z);
if (colorBand != null) {
int colorbandIndex = (int) (pixel.intensity * 1000.0f);
pixel.red = colorBand[colorbandIndex][0];
pixel.green = colorBand[colorbandIndex][1];
pixel.blue = colorBand[colorbandIndex][2];
this.applyBrightnessAndContrast(bacd, pixel);
} else {
this.applyBrightnessAndContrast(pixel, bacd.contrast, bacd.brightness);
}
}
private static interface IntensityFunction {
float getIntensity(float x, float y, float z);
}
}

@ -1,121 +0,0 @@
/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins.blender.textures.generating;
import com.jme3.math.FastMath;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.textures.TexturePixel;
import com.jme3.scene.plugins.blender.textures.generating.NoiseGenerator.NoiseFunction;
import com.jme3.texture.Image.Format;
/**
* This class generates the 'clouds' texture.
* @author Marcin Roguski (Kaelthas)
*/
public class TextureGeneratorClouds extends TextureGenerator {
// noiseType
protected static final int TEX_NOISESOFT = 0;
protected static final int TEX_NOISEPERL = 1;
// sType
protected static final int TEX_DEFAULT = 0;
protected static final int TEX_COLOR = 1;
protected float noisesize;
protected int noiseDepth;
protected int noiseBasis;
protected NoiseFunction noiseFunction;
protected int noiseType;
protected boolean isHard;
protected int sType;
/**
* Constructor stores the given noise generator.
* @param noiseGenerator
* the noise generator
*/
public TextureGeneratorClouds(NoiseGenerator noiseGenerator) {
super(noiseGenerator, Format.Luminance8);
}
@Override
public void readData(Structure tex, BlenderContext blenderContext) {
super.readData(tex, blenderContext);
noisesize = ((Number) tex.getFieldValue("noisesize")).floatValue();
noiseDepth = ((Number) tex.getFieldValue("noisedepth")).intValue();
noiseBasis = ((Number) tex.getFieldValue("noisebasis")).intValue();
noiseType = ((Number) tex.getFieldValue("noisetype")).intValue();
isHard = noiseType != TEX_NOISESOFT;
sType = ((Number) tex.getFieldValue("stype")).intValue();
if (sType == TEX_COLOR) {
imageFormat = Format.RGBA8;
}
noiseFunction = NoiseGenerator.noiseFunctions.get(noiseBasis);
if (noiseFunction == null) {
noiseFunction = NoiseGenerator.noiseFunctions.get(0);
noiseBasis = 0;
}
}
@Override
public void getPixel(TexturePixel pixel, float x, float y, float z) {
if (noiseBasis == 0) {
++x;
++y;
++z;
}
pixel.intensity = NoiseGenerator.NoiseFunctions.turbulence(x, y, z, noisesize, noiseDepth, noiseFunction, isHard);
if (colorBand != null) {
int colorbandIndex = (int) (pixel.intensity * 1000.0f);
pixel.red = colorBand[colorbandIndex][0];
pixel.green = colorBand[colorbandIndex][1];
pixel.blue = colorBand[colorbandIndex][2];
pixel.alpha = colorBand[colorbandIndex][3];
this.applyBrightnessAndContrast(bacd, pixel);
} else if (sType == TEX_COLOR) {
pixel.red = pixel.intensity;
pixel.green = NoiseGenerator.NoiseFunctions.turbulence(y, x, z, noisesize, noiseDepth, noiseFunction, isHard);
pixel.blue = NoiseGenerator.NoiseFunctions.turbulence(y, z, x, noisesize, noiseDepth, noiseFunction, isHard);
pixel.green = FastMath.clamp(pixel.green, 0.0f, 1.0f);
pixel.blue = FastMath.clamp(pixel.blue, 0.0f, 1.0f);
pixel.alpha = 1.0f;
this.applyBrightnessAndContrast(bacd, pixel);
} else {
this.applyBrightnessAndContrast(pixel, bacd.contrast, bacd.brightness);
}
}
}

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

Loading…
Cancel
Save