Compare commits
1 Commits
master
...
fix-openal
Author | SHA1 | Date |
---|---|---|
Riccardo Balbo | 3db1adbb54 | 5 years ago |
@ -1,29 +0,0 @@ |
||||
Copyright (c) 2009-2020 jMonkeyEngine |
||||
All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are |
||||
met: |
||||
|
||||
* Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
* Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
* Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
may be used to endorse or promote products derived from this software |
||||
without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,12 @@ |
||||
if (!hasProperty('mainClass')) { |
||||
ext.mainClass = '' |
||||
} |
||||
|
||||
dependencies { |
||||
compile project(':jme3-core') |
||||
compile project(':jme3-desktop') |
||||
compile project(':jme3-effects') |
||||
compile ('org.ejml:core:0.27') |
||||
compile ('org.ejml:dense64:0.27') |
||||
compile ('org.ejml:simple:0.27') |
||||
} |
@ -0,0 +1,733 @@ |
||||
/* |
||||
* Copyright (c) 2009-2018 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.asset; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.jme3.export.InputCapsule; |
||||
import com.jme3.export.JmeExporter; |
||||
import com.jme3.export.JmeImporter; |
||||
import com.jme3.export.OutputCapsule; |
||||
import com.jme3.material.Material; |
||||
import com.jme3.material.RenderState.FaceCullMode; |
||||
|
||||
/** |
||||
* Blender key. Contains path of the blender file and its loading properties. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BlenderKey extends ModelKey { |
||||
protected static final int DEFAULT_FPS = 25; |
||||
/** |
||||
* FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time |
||||
* between the frames. |
||||
*/ |
||||
protected int fps = DEFAULT_FPS; |
||||
/** |
||||
* This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded. |
||||
*/ |
||||
protected int featuresToLoad = FeaturesToLoad.ALL; |
||||
/** The variable that tells if content of the file (along with data unlinked to any feature on the scene) should be stored as 'user data' in the result spatial. */ |
||||
protected boolean loadUnlinkedAssets; |
||||
/** The root path for all the assets. */ |
||||
protected String assetRootPath; |
||||
/** This variable indicate if Y axis is UP axis. If not then Z is up. By default set to true. */ |
||||
protected boolean fixUpAxis = true; |
||||
/** Generated textures resolution (PPU - Pixels Per Unit). */ |
||||
protected int generatedTexturePPU = 128; |
||||
/** |
||||
* The name of world settings that the importer will use. If not set or specified name does not occur in the file |
||||
* then the first world settings in the file will be used. |
||||
*/ |
||||
protected String usedWorld; |
||||
/** |
||||
* User's default material that is set for objects that have no material definition in blender. The default value is |
||||
* null. If the value is null the importer will use its own default material (gray color - like in blender). |
||||
*/ |
||||
protected Material defaultMaterial; |
||||
/** Face cull mode. By default it is disabled. */ |
||||
protected FaceCullMode faceCullMode = FaceCullMode.Back; |
||||
/** |
||||
* Variable describes which layers will be loaded. N-th bit set means N-th layer will be loaded. |
||||
* If set to -1 then the current layer will be loaded. |
||||
*/ |
||||
protected int layersToLoad = -1; |
||||
/** A variable that toggles the object custom properties loading. */ |
||||
protected boolean loadObjectProperties = true; |
||||
/** |
||||
* Maximum texture size. Might be dependant on the graphic card. |
||||
* This value is taken from <b>org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE</b>. |
||||
*/ |
||||
protected int maxTextureSize = 8192; |
||||
/** Allows to toggle generated textures loading. Disabled by default because it very often takes too much memory and needs to be used wisely. */ |
||||
protected boolean loadGeneratedTextures; |
||||
/** Tells if the mipmaps will be generated by jme or not. By default generation is dependant on the blender settings. */ |
||||
protected MipmapGenerationMethod mipmapGenerationMethod = MipmapGenerationMethod.GENERATE_WHEN_NEEDED; |
||||
/** |
||||
* If the sky has only generated textures applied then they will have the following size (both width and height). If 2d textures are used then the generated |
||||
* textures will get their proper size. |
||||
*/ |
||||
protected int skyGeneratedTextureSize = 1000; |
||||
/** The radius of a shape that will be used while creating the generated texture for the sky. The higher it is the larger part of the texture will be seen. */ |
||||
protected float skyGeneratedTextureRadius = 1; |
||||
/** The shape against which the generated texture for the sky will be created. */ |
||||
protected SkyGeneratedTextureShape skyGeneratedTextureShape = SkyGeneratedTextureShape.SPHERE; |
||||
/** |
||||
* This field tells if the importer should optimise the use of textures or not. If set to true, then textures of the same mapping type will be merged together |
||||
* and textures that in the final result will never be visible - will be discarded. |
||||
*/ |
||||
protected boolean optimiseTextures; |
||||
/** The method of matching animations to skeletons. The default value is: AT_LEAST_ONE_NAME_MATCH. */ |
||||
protected AnimationMatchMethod animationMatchMethod = AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH; |
||||
/** The size of points that are loaded and do not belong to any edge of the mesh. */ |
||||
protected float pointsSize = 1; |
||||
/** The width of edges that are loaded from the mesh and do not belong to any face. */ |
||||
protected float linesWidth = 1; |
||||
|
||||
/** |
||||
* Constructor used by serialization mechanisms. |
||||
*/ |
||||
public BlenderKey() { |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Creates a key for the given file name. |
||||
* @param name |
||||
* the name (path) of a file |
||||
*/ |
||||
public BlenderKey(String name) { |
||||
super(name); |
||||
} |
||||
|
||||
/** |
||||
* This method returns frames per second amount. The default value is BlenderKey.DEFAULT_FPS = 25. |
||||
* @return the frames per second amount |
||||
*/ |
||||
public int getFps() { |
||||
return fps; |
||||
} |
||||
|
||||
/** |
||||
* This method sets frames per second amount. |
||||
* @param fps |
||||
* the frames per second amount |
||||
*/ |
||||
public void setFps(int fps) { |
||||
this.fps = fps; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the face cull mode. |
||||
* @return the face cull mode |
||||
*/ |
||||
public FaceCullMode getFaceCullMode() { |
||||
return faceCullMode; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the face cull mode. |
||||
* @param faceCullMode |
||||
* the face cull mode |
||||
*/ |
||||
public void setFaceCullMode(FaceCullMode faceCullMode) { |
||||
this.faceCullMode = faceCullMode; |
||||
} |
||||
|
||||
/** |
||||
* This method sets layers to be loaded. |
||||
* @param layersToLoad |
||||
* layers to be loaded |
||||
*/ |
||||
public void setLayersToLoad(int layersToLoad) { |
||||
this.layersToLoad = layersToLoad; |
||||
} |
||||
|
||||
/** |
||||
* This method returns layers to be loaded. |
||||
* @return layers to be loaded |
||||
*/ |
||||
public int getLayersToLoad() { |
||||
return layersToLoad; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the properies loading policy. |
||||
* By default the value is true. |
||||
* @param loadObjectProperties |
||||
* true to load properties and false to suspend their loading |
||||
*/ |
||||
public void setLoadObjectProperties(boolean loadObjectProperties) { |
||||
this.loadObjectProperties = loadObjectProperties; |
||||
} |
||||
|
||||
/** |
||||
* @return the current properties loading properties |
||||
*/ |
||||
public boolean isLoadObjectProperties() { |
||||
return loadObjectProperties; |
||||
} |
||||
|
||||
/** |
||||
* The default value for this parameter is the same as defined by: org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE. |
||||
* If by any means this is too large for user's hardware configuration use the 'setMaxTextureSize' method to change that. |
||||
* @return maximum texture size (width/height) |
||||
*/ |
||||
public int getMaxTextureSize() { |
||||
return maxTextureSize; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the maximum texture size. |
||||
* @param maxTextureSize |
||||
* the maximum texture size |
||||
*/ |
||||
public void setMaxTextureSize(int maxTextureSize) { |
||||
this.maxTextureSize = maxTextureSize; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the flag that toggles the generated textures loading. |
||||
* @param loadGeneratedTextures |
||||
* <b>true</b> if generated textures should be loaded and <b>false</b> otherwise |
||||
*/ |
||||
public void setLoadGeneratedTextures(boolean loadGeneratedTextures) { |
||||
this.loadGeneratedTextures = loadGeneratedTextures; |
||||
} |
||||
|
||||
/** |
||||
* @return tells if the generated textures should be loaded (<b>false</b> is the default value) |
||||
*/ |
||||
public boolean isLoadGeneratedTextures() { |
||||
return loadGeneratedTextures; |
||||
} |
||||
|
||||
/** |
||||
* Not used any more. |
||||
* This method sets the asset root path. |
||||
* @param assetRootPath |
||||
* the assets root path |
||||
*/ |
||||
@Deprecated |
||||
public void setAssetRootPath(String assetRootPath) { |
||||
this.assetRootPath = assetRootPath; |
||||
} |
||||
|
||||
/** |
||||
* Not used any more. |
||||
* This method returns the asset root path. |
||||
* @return the asset root path |
||||
*/ |
||||
@Deprecated |
||||
public String getAssetRootPath() { |
||||
return assetRootPath; |
||||
} |
||||
|
||||
/** |
||||
* This method adds features to be loaded. |
||||
* @param featuresToLoad |
||||
* bitwise flag of FeaturesToLoad interface values |
||||
*/ |
||||
@Deprecated |
||||
public void includeInLoading(int featuresToLoad) { |
||||
this.featuresToLoad |= featuresToLoad; |
||||
} |
||||
|
||||
/** |
||||
* This method removes features from being loaded. |
||||
* @param featuresNotToLoad |
||||
* bitwise flag of FeaturesToLoad interface values |
||||
*/ |
||||
@Deprecated |
||||
public void excludeFromLoading(int featuresNotToLoad) { |
||||
featuresToLoad &= ~featuresNotToLoad; |
||||
} |
||||
|
||||
@Deprecated |
||||
public boolean shouldLoad(int featureToLoad) { |
||||
return (featuresToLoad & featureToLoad) != 0; |
||||
} |
||||
|
||||
/** |
||||
* This method returns bitwise value of FeaturesToLoad interface value. It describes features that will be loaded by |
||||
* the blender file loader. |
||||
* @return features that will be loaded by the blender file loader |
||||
*/ |
||||
@Deprecated |
||||
public int getFeaturesToLoad() { |
||||
return featuresToLoad; |
||||
} |
||||
|
||||
/** |
||||
* This method determines if unlinked assets should be loaded. |
||||
* If not then only objects on selected layers will be loaded and their assets if required. |
||||
* If yes then all assets will be loaded even if they are on inactive layers or are not linked |
||||
* to anything. |
||||
* @return <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isLoadUnlinkedAssets() { |
||||
return loadUnlinkedAssets; |
||||
} |
||||
|
||||
/** |
||||
* This method sets if unlinked assets should be loaded. |
||||
* If not then only objects on selected layers will be loaded and their assets if required. |
||||
* If yes then all assets will be loaded even if they are on inactive layers or are not linked |
||||
* to anything. |
||||
* @param loadUnlinkedAssets |
||||
* <b>true</b> if unlinked assets should be loaded and <b>false</b> otherwise |
||||
*/ |
||||
public void setLoadUnlinkedAssets(boolean loadUnlinkedAssets) { |
||||
this.loadUnlinkedAssets = loadUnlinkedAssets; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By default Y |
||||
* is up axis. |
||||
* @param fixUpAxis |
||||
* the up axis state variable |
||||
*/ |
||||
public void setFixUpAxis(boolean fixUpAxis) { |
||||
this.fixUpAxis = fixUpAxis; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the fix up axis state. If set to true then Y is up axis. Otherwise the up i Z axis. By |
||||
* default Y is up axis. |
||||
* @return the up axis state variable |
||||
*/ |
||||
public boolean isFixUpAxis() { |
||||
return fixUpAxis; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the generated textures resolution. |
||||
* @param generatedTexturePPU |
||||
* the generated textures resolution |
||||
*/ |
||||
public void setGeneratedTexturePPU(int generatedTexturePPU) { |
||||
this.generatedTexturePPU = generatedTexturePPU; |
||||
} |
||||
|
||||
/** |
||||
* @return the generated textures resolution |
||||
*/ |
||||
public int getGeneratedTexturePPU() { |
||||
return generatedTexturePPU; |
||||
} |
||||
|
||||
/** |
||||
* @return mipmaps generation method |
||||
*/ |
||||
public MipmapGenerationMethod getMipmapGenerationMethod() { |
||||
return mipmapGenerationMethod; |
||||
} |
||||
|
||||
/** |
||||
* @param mipmapGenerationMethod |
||||
* mipmaps generation method |
||||
*/ |
||||
public void setMipmapGenerationMethod(MipmapGenerationMethod mipmapGenerationMethod) { |
||||
this.mipmapGenerationMethod = mipmapGenerationMethod; |
||||
} |
||||
|
||||
/** |
||||
* @return the size of the generated textures for the sky (used if no flat textures are applied) |
||||
*/ |
||||
public int getSkyGeneratedTextureSize() { |
||||
return skyGeneratedTextureSize; |
||||
} |
||||
|
||||
/** |
||||
* @param skyGeneratedTextureSize |
||||
* the size of the generated textures for the sky (used if no flat textures are applied) |
||||
*/ |
||||
public void setSkyGeneratedTextureSize(int skyGeneratedTextureSize) { |
||||
if (skyGeneratedTextureSize <= 0) { |
||||
throw new IllegalArgumentException("The texture size must be a positive value (the value given as a parameter: " + skyGeneratedTextureSize + ")!"); |
||||
} |
||||
this.skyGeneratedTextureSize = skyGeneratedTextureSize; |
||||
} |
||||
|
||||
/** |
||||
* @return the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen |
||||
*/ |
||||
public float getSkyGeneratedTextureRadius() { |
||||
return skyGeneratedTextureRadius; |
||||
} |
||||
|
||||
/** |
||||
* @param skyGeneratedTextureRadius |
||||
* the radius of a shape that will be used while creating the generated texture for the sky, the higher it is the larger part of the texture will be seen |
||||
*/ |
||||
public void setSkyGeneratedTextureRadius(float skyGeneratedTextureRadius) { |
||||
this.skyGeneratedTextureRadius = skyGeneratedTextureRadius; |
||||
} |
||||
|
||||
/** |
||||
* @return the shape against which the generated texture for the sky will be created (by default it is a sphere). |
||||
*/ |
||||
public SkyGeneratedTextureShape getSkyGeneratedTextureShape() { |
||||
return skyGeneratedTextureShape; |
||||
} |
||||
|
||||
/** |
||||
* @param skyGeneratedTextureShape |
||||
* the shape against which the generated texture for the sky will be created |
||||
*/ |
||||
public void setSkyGeneratedTextureShape(SkyGeneratedTextureShape skyGeneratedTextureShape) { |
||||
if (skyGeneratedTextureShape == null) { |
||||
throw new IllegalArgumentException("The sky generated shape type cannot be null!"); |
||||
} |
||||
this.skyGeneratedTextureShape = skyGeneratedTextureShape; |
||||
} |
||||
|
||||
/** |
||||
* If set to true, then textures of the same mapping type will be merged together |
||||
* and textures that in the final result will never be visible - will be discarded. |
||||
* @param optimiseTextures |
||||
* the variable that tells if the textures should be optimised or not |
||||
*/ |
||||
public void setOptimiseTextures(boolean optimiseTextures) { |
||||
this.optimiseTextures = optimiseTextures; |
||||
} |
||||
|
||||
/** |
||||
* @return the variable that tells if the textures should be optimised or not (by default the optimisation is disabled) |
||||
*/ |
||||
public boolean isOptimiseTextures() { |
||||
return optimiseTextures; |
||||
} |
||||
|
||||
/** |
||||
* Sets the way the animations will be matched with skeletons. |
||||
* |
||||
* @param animationMatchMethod |
||||
* the way the animations will be matched with skeletons |
||||
*/ |
||||
public void setAnimationMatchMethod(AnimationMatchMethod animationMatchMethod) { |
||||
this.animationMatchMethod = animationMatchMethod; |
||||
} |
||||
|
||||
/** |
||||
* @return the way the animations will be matched with skeletons |
||||
*/ |
||||
public AnimationMatchMethod getAnimationMatchMethod() { |
||||
return animationMatchMethod; |
||||
} |
||||
|
||||
/** |
||||
* @return the size of points that are loaded and do not belong to any edge of the mesh |
||||
*/ |
||||
public float getPointsSize() { |
||||
return pointsSize; |
||||
} |
||||
|
||||
/** |
||||
* Sets the size of points that are loaded and do not belong to any edge of the mesh. |
||||
* @param pointsSize |
||||
* The size of points that are loaded and do not belong to any edge of the mesh |
||||
*/ |
||||
public void setPointsSize(float pointsSize) { |
||||
this.pointsSize = pointsSize; |
||||
} |
||||
|
||||
/** |
||||
* @return the width of edges that are loaded from the mesh and do not belong to any face |
||||
*/ |
||||
public float getLinesWidth() { |
||||
return linesWidth; |
||||
} |
||||
|
||||
/** |
||||
* Sets the width of edges that are loaded from the mesh and do not belong to any face. |
||||
* @param linesWidth |
||||
* the width of edges that are loaded from the mesh and do not belong to any face |
||||
*/ |
||||
public void setLinesWidth(float linesWidth) { |
||||
this.linesWidth = linesWidth; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the name of the WORLD data block that should be used during file loading. By default the name is |
||||
* not set. If no name is set or the given name does not occur in the file - the first WORLD data block will be used |
||||
* during loading (assuming any exists in the file). |
||||
* @param usedWorld |
||||
* the name of the WORLD block used during loading |
||||
*/ |
||||
public void setUsedWorld(String usedWorld) { |
||||
this.usedWorld = usedWorld; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the name of the WORLD data block that should be used during file loading. |
||||
* @return the name of the WORLD block used during loading |
||||
*/ |
||||
public String getUsedWorld() { |
||||
return usedWorld; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the default material for objects. |
||||
* @param defaultMaterial |
||||
* the default material |
||||
*/ |
||||
public void setDefaultMaterial(Material defaultMaterial) { |
||||
this.defaultMaterial = defaultMaterial; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the default material. |
||||
* @return the default material |
||||
*/ |
||||
public Material getDefaultMaterial() { |
||||
return defaultMaterial; |
||||
} |
||||
|
||||
@Override |
||||
public void write(JmeExporter e) throws IOException { |
||||
super.write(e); |
||||
OutputCapsule oc = e.getCapsule(this); |
||||
oc.write(fps, "fps", DEFAULT_FPS); |
||||
oc.write(featuresToLoad, "features-to-load", FeaturesToLoad.ALL); |
||||
oc.write(loadUnlinkedAssets, "load-unlinked-assets", false); |
||||
oc.write(assetRootPath, "asset-root-path", null); |
||||
oc.write(fixUpAxis, "fix-up-axis", true); |
||||
oc.write(generatedTexturePPU, "generated-texture-ppu", 128); |
||||
oc.write(usedWorld, "used-world", null); |
||||
oc.write(defaultMaterial, "default-material", null); |
||||
oc.write(faceCullMode, "face-cull-mode", FaceCullMode.Off); |
||||
oc.write(layersToLoad, "layers-to-load", -1); |
||||
oc.write(mipmapGenerationMethod, "mipmap-generation-method", MipmapGenerationMethod.GENERATE_WHEN_NEEDED); |
||||
oc.write(skyGeneratedTextureSize, "sky-generated-texture-size", 1000); |
||||
oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f); |
||||
oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE); |
||||
oc.write(optimiseTextures, "optimise-textures", false); |
||||
oc.write(animationMatchMethod, "animation-match-method", AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH); |
||||
oc.write(pointsSize, "points-size", 1); |
||||
oc.write(linesWidth, "lines-width", 1); |
||||
} |
||||
|
||||
@Override |
||||
public void read(JmeImporter e) throws IOException { |
||||
super.read(e); |
||||
InputCapsule ic = e.getCapsule(this); |
||||
fps = ic.readInt("fps", DEFAULT_FPS); |
||||
featuresToLoad = ic.readInt("features-to-load", FeaturesToLoad.ALL); |
||||
loadUnlinkedAssets = ic.readBoolean("load-unlinked-assets", false); |
||||
assetRootPath = ic.readString("asset-root-path", null); |
||||
fixUpAxis = ic.readBoolean("fix-up-axis", true); |
||||
generatedTexturePPU = ic.readInt("generated-texture-ppu", 128); |
||||
usedWorld = ic.readString("used-world", null); |
||||
defaultMaterial = (Material) ic.readSavable("default-material", null); |
||||
faceCullMode = ic.readEnum("face-cull-mode", FaceCullMode.class, FaceCullMode.Off); |
||||
layersToLoad = ic.readInt("layers-to=load", -1); |
||||
mipmapGenerationMethod = ic.readEnum("mipmap-generation-method", MipmapGenerationMethod.class, MipmapGenerationMethod.GENERATE_WHEN_NEEDED); |
||||
skyGeneratedTextureSize = ic.readInt("sky-generated-texture-size", 1000); |
||||
skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f); |
||||
skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE); |
||||
optimiseTextures = ic.readBoolean("optimise-textures", false); |
||||
animationMatchMethod = ic.readEnum("animation-match-method", AnimationMatchMethod.class, AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH); |
||||
pointsSize = ic.readFloat("points-size", 1); |
||||
linesWidth = ic.readFloat("lines-width", 1); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
final int prime = 31; |
||||
int result = super.hashCode(); |
||||
result = prime * result + (animationMatchMethod == null ? 0 : animationMatchMethod.hashCode()); |
||||
result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode()); |
||||
result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode()); |
||||
result = prime * result + (faceCullMode == null ? 0 : faceCullMode.hashCode()); |
||||
result = prime * result + featuresToLoad; |
||||
result = prime * result + (fixUpAxis ? 1231 : 1237); |
||||
result = prime * result + fps; |
||||
result = prime * result + generatedTexturePPU; |
||||
result = prime * result + layersToLoad; |
||||
result = prime * result + (loadGeneratedTextures ? 1231 : 1237); |
||||
result = prime * result + (loadObjectProperties ? 1231 : 1237); |
||||
result = prime * result + (loadUnlinkedAssets ? 1231 : 1237); |
||||
result = prime * result + maxTextureSize; |
||||
result = prime * result + (mipmapGenerationMethod == null ? 0 : mipmapGenerationMethod.hashCode()); |
||||
result = prime * result + (optimiseTextures ? 1231 : 1237); |
||||
result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius); |
||||
result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode()); |
||||
result = prime * result + skyGeneratedTextureSize; |
||||
result = prime * result + (usedWorld == null ? 0 : usedWorld.hashCode()); |
||||
result = prime * result + (int) pointsSize; |
||||
result = prime * result + (int) linesWidth; |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (obj instanceof BlenderKey) { |
||||
return false; |
||||
} |
||||
BlenderKey other = (BlenderKey) obj; |
||||
if (animationMatchMethod != other.animationMatchMethod) { |
||||
return false; |
||||
} |
||||
if (assetRootPath == null) { |
||||
if (other.assetRootPath != null) { |
||||
return false; |
||||
} |
||||
} else if (!assetRootPath.equals(other.assetRootPath)) { |
||||
return false; |
||||
} |
||||
if (defaultMaterial == null) { |
||||
if (other.defaultMaterial != null) { |
||||
return false; |
||||
} |
||||
} else if (!defaultMaterial.equals(other.defaultMaterial)) { |
||||
return false; |
||||
} |
||||
if (faceCullMode != other.faceCullMode) { |
||||
return false; |
||||
} |
||||
if (featuresToLoad != other.featuresToLoad) { |
||||
return false; |
||||
} |
||||
if (fixUpAxis != other.fixUpAxis) { |
||||
return false; |
||||
} |
||||
if (fps != other.fps) { |
||||
return false; |
||||
} |
||||
if (generatedTexturePPU != other.generatedTexturePPU) { |
||||
return false; |
||||
} |
||||
if (layersToLoad != other.layersToLoad) { |
||||
return false; |
||||
} |
||||
if (loadGeneratedTextures != other.loadGeneratedTextures) { |
||||
return false; |
||||
} |
||||
if (loadObjectProperties != other.loadObjectProperties) { |
||||
return false; |
||||
} |
||||
if (loadUnlinkedAssets != other.loadUnlinkedAssets) { |
||||
return false; |
||||
} |
||||
if (maxTextureSize != other.maxTextureSize) { |
||||
return false; |
||||
} |
||||
if (mipmapGenerationMethod != other.mipmapGenerationMethod) { |
||||
return false; |
||||
} |
||||
if (optimiseTextures != other.optimiseTextures) { |
||||
return false; |
||||
} |
||||
if (Float.floatToIntBits(skyGeneratedTextureRadius) != Float.floatToIntBits(other.skyGeneratedTextureRadius)) { |
||||
return false; |
||||
} |
||||
if (skyGeneratedTextureShape != other.skyGeneratedTextureShape) { |
||||
return false; |
||||
} |
||||
if (skyGeneratedTextureSize != other.skyGeneratedTextureSize) { |
||||
return false; |
||||
} |
||||
if (usedWorld == null) { |
||||
if (other.usedWorld != null) { |
||||
return false; |
||||
} |
||||
} else if (!usedWorld.equals(other.usedWorld)) { |
||||
return false; |
||||
} |
||||
if (pointsSize != other.pointsSize) { |
||||
return false; |
||||
} |
||||
if (linesWidth != other.linesWidth) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* This enum tells the importer if the mipmaps for textures will be generated by jme. <li>NEVER_GENERATE and ALWAYS_GENERATE are quite understandable <li>GENERATE_WHEN_NEEDED is an option that checks if the texture had 'Generate mipmaps' option set in blender, mipmaps are generated only when the option is set |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static enum MipmapGenerationMethod { |
||||
NEVER_GENERATE, ALWAYS_GENERATE, GENERATE_WHEN_NEEDED; |
||||
} |
||||
|
||||
/** |
||||
* This interface describes the features of the scene that are to be loaded. |
||||
* @deprecated this interface is deprecated and is not used anymore; to ensure the loading models consistency |
||||
* everything must be loaded because in blender one feature might depend on another |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
@Deprecated |
||||
public static interface FeaturesToLoad { |
||||
|
||||
int SCENES = 0x0000FFFF; |
||||
int OBJECTS = 0x0000000B; |
||||
int ANIMATIONS = 0x00000004; |
||||
int MATERIALS = 0x00000003; |
||||
int TEXTURES = 0x00000001; |
||||
int CAMERAS = 0x00000020; |
||||
int LIGHTS = 0x00000010; |
||||
int WORLD = 0x00000040; |
||||
int ALL = 0xFFFFFFFF; |
||||
} |
||||
|
||||
/** |
||||
* The shape againts which the sky generated texture will be created. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static enum SkyGeneratedTextureShape { |
||||
CUBE, SPHERE; |
||||
} |
||||
|
||||
/** |
||||
* This enum describes which animations should be attached to which armature. |
||||
* Blender does not store the mapping between action and armature. That is why the importer |
||||
* will try to match those by comparing bone name of the armature with the channel names |
||||
* int the actions. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static enum AnimationMatchMethod { |
||||
/** |
||||
* Animation is matched with skeleton when at leas one bone name matches the name of the action channel. |
||||
* All the bones that do not have their corresponding channel in the animation will not get the proper tracks for |
||||
* this particulat animation. |
||||
* Also the channel will not be used for the animation if it does not find the proper bone name. |
||||
*/ |
||||
AT_LEAST_ONE_NAME_MATCH, |
||||
/** |
||||
* Animation is matched when all action names are covered by the target names (bone names or the name of the |
||||
* animated spatial. |
||||
*/ |
||||
ALL_NAMES_MATCH; |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.asset; |
||||
|
||||
/** |
||||
* This key is mostly used to distinguish between textures that are loaded from |
||||
* the given assets and those being generated automatically. Every generated |
||||
* texture will have this kind of key attached. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class GeneratedTextureKey extends TextureKey { |
||||
|
||||
/** |
||||
* Constructor. Stores the name. Extension and folder name are empty |
||||
* strings. |
||||
* |
||||
* @param name |
||||
* the name of the texture |
||||
*/ |
||||
public GeneratedTextureKey(String name) { |
||||
super(name); |
||||
} |
||||
|
||||
@Override |
||||
public String getExtension() { |
||||
return ""; |
||||
} |
||||
|
||||
@Override |
||||
public String getFolder() { |
||||
return ""; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "Generated texture [" + name + "]"; |
||||
} |
||||
} |
@ -0,0 +1,193 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.asset.AssetNotFoundException; |
||||
import com.jme3.asset.BlenderKey; |
||||
import com.jme3.export.Savable; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.objects.Properties; |
||||
|
||||
/** |
||||
* A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can |
||||
* hold the state of the calculations. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public abstract class AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(AbstractBlenderHelper.class.getName()); |
||||
|
||||
/** The blender context. */ |
||||
protected BlenderContext blenderContext; |
||||
/** The version of the blend file. */ |
||||
protected final int blenderVersion; |
||||
/** This variable indicates if the Y asxis is the UP axis or not. */ |
||||
protected boolean fixUpAxis; |
||||
/** Quaternion used to rotate data when Y is up axis. */ |
||||
protected Quaternion upAxisRotationQuaternion; |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender |
||||
* versions. |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public AbstractBlenderHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
this.blenderVersion = Integer.parseInt(blenderVersion); |
||||
this.blenderContext = blenderContext; |
||||
fixUpAxis = blenderContext.getBlenderKey().isFixUpAxis(); |
||||
if (fixUpAxis) { |
||||
upAxisRotationQuaternion = new Quaternion().fromAngles(-FastMath.HALF_PI, 0, 0); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method loads the properties if they are available and defined for the structure. |
||||
* @param structure |
||||
* the structure we read the properties from |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return loaded properties or null if they are not available |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when the blend file is somehow corrupted |
||||
*/ |
||||
protected Properties loadProperties(Structure structure, BlenderContext blenderContext) throws BlenderFileException { |
||||
Properties properties = null; |
||||
Structure id = (Structure) structure.getFieldValue("ID"); |
||||
if (id != null) { |
||||
Pointer pProperties = (Pointer) id.getFieldValue("properties"); |
||||
if (pProperties.isNotNull()) { |
||||
Structure propertiesStructure = pProperties.fetchData().get(0); |
||||
properties = new Properties(); |
||||
properties.load(propertiesStructure, blenderContext); |
||||
} |
||||
} |
||||
return properties; |
||||
} |
||||
|
||||
/** |
||||
* The method applies properties to the given spatial. The Properties |
||||
* instance cannot be directly applied because the end-user might not have |
||||
* the blender plugin jar file and thus receive ClassNotFoundException. The |
||||
* values are set by name instead. |
||||
* |
||||
* @param spatial |
||||
* the spatial that is to have properties applied |
||||
* @param properties |
||||
* the properties to be applied |
||||
*/ |
||||
public void applyProperties(Spatial spatial, Properties properties) { |
||||
List<String> propertyNames = properties.getSubPropertiesNames(); |
||||
if (propertyNames != null && propertyNames.size() > 0) { |
||||
for (String propertyName : propertyNames) { |
||||
Object value = properties.findValue(propertyName); |
||||
if (value instanceof Savable || value instanceof Boolean || value instanceof String || value instanceof Float || value instanceof Integer || value instanceof Long) { |
||||
spatial.setUserData(propertyName, value); |
||||
} else if (value instanceof Double) { |
||||
spatial.setUserData(propertyName, ((Double) value).floatValue()); |
||||
} else if (value instanceof int[]) { |
||||
spatial.setUserData(propertyName, Arrays.toString((int[]) value)); |
||||
} else if (value instanceof float[]) { |
||||
spatial.setUserData(propertyName, Arrays.toString((float[]) value)); |
||||
} else if (value instanceof double[]) { |
||||
spatial.setUserData(propertyName, Arrays.toString((double[]) value)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method loads library of a given ID from linked blender file. |
||||
* @param id |
||||
* the ID of the linked feature (it contains its name and blender path) |
||||
* @return loaded feature or null if none was found |
||||
* @throws BlenderFileException |
||||
* and exception is throw when problems with reading a blend file occur |
||||
*/ |
||||
protected Object loadLibrary(Structure id) throws BlenderFileException { |
||||
Pointer pLib = (Pointer) id.getFieldValue("lib"); |
||||
if (pLib.isNotNull()) { |
||||
String fullName = id.getFieldValue("name").toString();// we need full name with the prefix
|
||||
String nameOfFeatureToLoad = id.getName(); |
||||
Structure library = pLib.fetchData().get(0); |
||||
String path = library.getFieldValue("filepath").toString(); |
||||
|
||||
if (!blenderContext.getLinkedFeatures().keySet().contains(path)) { |
||||
Spatial loadedAsset = null; |
||||
BlenderKey blenderKey = new BlenderKey(path); |
||||
blenderKey.setLoadUnlinkedAssets(true); |
||||
try { |
||||
loadedAsset = blenderContext.getAssetManager().loadAsset(blenderKey); |
||||
} catch (AssetNotFoundException e) { |
||||
LOGGER.log(Level.FINEST, "Cannot locate linked resource at path: {0}.", path); |
||||
} |
||||
|
||||
if (loadedAsset != null) { |
||||
Map<String, Map<String, Object>> linkedData = loadedAsset.getUserData("linkedData"); |
||||
|
||||
for (Entry<String, Map<String, Object>> entry : linkedData.entrySet()) { |
||||
String linkedDataFilePath = "this".equals(entry.getKey()) ? path : entry.getKey(); |
||||
blenderContext.getLinkedFeatures().put(linkedDataFilePath, entry.getValue()); |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "No features loaded from path: {0}.", path); |
||||
} |
||||
} |
||||
|
||||
Object result = blenderContext.getLinkedFeature(path, fullName); |
||||
if (result == null) { |
||||
LOGGER.log(Level.WARNING, "Could NOT find asset named {0} in the library of path: {1}.", new Object[] { nameOfFeatureToLoad, path }); |
||||
} else { |
||||
blenderContext.addLoadedFeatures(id.getOldMemoryAddress(), LoadedDataType.STRUCTURE, id); |
||||
blenderContext.addLoadedFeatures(id.getOldMemoryAddress(), LoadedDataType.FEATURE, result); |
||||
} |
||||
return result; |
||||
} else { |
||||
LOGGER.warning("Library link points to nothing!"); |
||||
} |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,767 @@ |
||||
/* |
||||
* Copyright (c) 2009-2019 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.EmptyStackException; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.Stack; |
||||
|
||||
import com.jme3.animation.Animation; |
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.asset.AssetManager; |
||||
import com.jme3.asset.BlenderKey; |
||||
import com.jme3.light.Light; |
||||
import com.jme3.material.Material; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.post.Filter; |
||||
import com.jme3.renderer.Camera; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.animations.BlenderAction; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.Constraint; |
||||
import com.jme3.scene.plugins.blender.file.BlenderInputStream; |
||||
import com.jme3.scene.plugins.blender.file.DnaBlockData; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialContext; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.texture.Texture; |
||||
|
||||
/** |
||||
* The class that stores temporary data and manages it during loading the belnd |
||||
* file. This class is intended to be used in a single loading thread. It holds |
||||
* the state of loading operations. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BlenderContext { |
||||
/** The blender file version. */ |
||||
private int blenderVersion; |
||||
/** The blender key. */ |
||||
private BlenderKey blenderKey; |
||||
/** The header of the file block. */ |
||||
private DnaBlockData dnaBlockData; |
||||
/** The scene structure. */ |
||||
private Structure sceneStructure; |
||||
/** The input stream of the blend file. */ |
||||
private BlenderInputStream inputStream; |
||||
/** The asset manager. */ |
||||
private AssetManager assetManager; |
||||
/** The blocks read from the file. */ |
||||
protected List<FileBlockHeader> blocks = new ArrayList<FileBlockHeader>(); |
||||
/** |
||||
* A map containing the file block headers. The key is the old memory address. |
||||
*/ |
||||
private Map<Long, FileBlockHeader> fileBlockHeadersByOma = new HashMap<Long, FileBlockHeader>(); |
||||
/** A map containing the file block headers. The key is the block code. */ |
||||
private Map<BlockCode, List<FileBlockHeader>> fileBlockHeadersByCode = new HashMap<BlockCode, List<FileBlockHeader>>(); |
||||
/** |
||||
* This map stores the loaded features by their old memory address. The |
||||
* first object in the value table is the loaded structure and the second - |
||||
* the structure already converted into proper data. |
||||
*/ |
||||
private Map<Long, Map<LoadedDataType, Object>> loadedFeatures = new HashMap<Long, Map<LoadedDataType, Object>>(); |
||||
/** Features loaded from external blender files. The key is the file path and the value is a map between feature name and loaded feature. */ |
||||
private Map<String, Map<String, Object>> linkedFeatures = new HashMap<String, Map<String, Object>>(); |
||||
/** A stack that hold the parent structure of currently loaded feature. */ |
||||
private Stack<Structure> parentStack = new Stack<Structure>(); |
||||
/** A list of constraints for the specified object. */ |
||||
protected Map<Long, List<Constraint>> constraints = new HashMap<Long, List<Constraint>>(); |
||||
/** Animations loaded for features. */ |
||||
private Map<Long, List<Animation>> animations = new HashMap<Long, List<Animation>>(); |
||||
/** Loaded skeletons. */ |
||||
private Map<Long, Skeleton> skeletons = new HashMap<Long, Skeleton>(); |
||||
/** A map between skeleton and node it modifies. */ |
||||
private Map<Skeleton, Node> nodesWithSkeletons = new HashMap<Skeleton, Node>(); |
||||
/** A map of bone contexts. */ |
||||
protected Map<Long, BoneContext> boneContexts = new HashMap<Long, BoneContext>(); |
||||
/** A map og helpers that perform loading. */ |
||||
private Map<String, AbstractBlenderHelper> helpers = new HashMap<String, AbstractBlenderHelper>(); |
||||
/** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */ |
||||
private Map<String, Map<Object, Object>> markers = new HashMap<String, Map<Object, Object>>(); |
||||
/** A map of blender actions. The key is the action name and the value is the action itself. */ |
||||
private Map<String, BlenderAction> actions = new HashMap<String, BlenderAction>(); |
||||
|
||||
/** |
||||
* This method sets the blender file version. |
||||
* |
||||
* @param blenderVersion |
||||
* the blender file version |
||||
*/ |
||||
public void setBlenderVersion(String blenderVersion) { |
||||
this.blenderVersion = Integer.parseInt(blenderVersion); |
||||
} |
||||
|
||||
/** |
||||
* @return the blender file version |
||||
*/ |
||||
public int getBlenderVersion() { |
||||
return blenderVersion; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the blender key. |
||||
* |
||||
* @param blenderKey |
||||
* the blender key |
||||
*/ |
||||
public void setBlenderKey(BlenderKey blenderKey) { |
||||
this.blenderKey = blenderKey; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the blender key. |
||||
* |
||||
* @return the blender key |
||||
*/ |
||||
public BlenderKey getBlenderKey() { |
||||
return blenderKey; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the dna block data. |
||||
* |
||||
* @param dnaBlockData |
||||
* the dna block data |
||||
*/ |
||||
public void setBlockData(DnaBlockData dnaBlockData) { |
||||
this.dnaBlockData = dnaBlockData; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the dna block data. |
||||
* |
||||
* @return the dna block data |
||||
*/ |
||||
public DnaBlockData getDnaBlockData() { |
||||
return dnaBlockData; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the scene structure data. |
||||
* |
||||
* @param sceneStructure |
||||
* the scene structure data |
||||
*/ |
||||
public void setSceneStructure(Structure sceneStructure) { |
||||
this.sceneStructure = sceneStructure; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the scene structure data. |
||||
* |
||||
* @return the scene structure data |
||||
*/ |
||||
public Structure getSceneStructure() { |
||||
return sceneStructure; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the asset manager. |
||||
* |
||||
* @return the asset manager |
||||
*/ |
||||
public AssetManager getAssetManager() { |
||||
return assetManager; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the asset manager. |
||||
* |
||||
* @param assetManager |
||||
* the asset manager |
||||
*/ |
||||
public void setAssetManager(AssetManager assetManager) { |
||||
this.assetManager = assetManager; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the input stream of the blend file. |
||||
* |
||||
* @return the input stream of the blend file |
||||
*/ |
||||
public BlenderInputStream getInputStream() { |
||||
return inputStream; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the input stream of the blend file. |
||||
* |
||||
* @param inputStream |
||||
* the input stream of the blend file |
||||
*/ |
||||
public void setInputStream(BlenderInputStream inputStream) { |
||||
this.inputStream = inputStream; |
||||
} |
||||
|
||||
/** |
||||
* This method adds a file block header to the map. Its old memory address |
||||
* is the key. |
||||
* |
||||
* @param oldMemoryAddress |
||||
* the address of the block header |
||||
* @param fileBlockHeader |
||||
* the block header to store |
||||
*/ |
||||
public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) { |
||||
blocks.add(fileBlockHeader); |
||||
fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader); |
||||
List<FileBlockHeader> headers = fileBlockHeadersByCode.get(fileBlockHeader.getCode()); |
||||
if (headers == null) { |
||||
headers = new ArrayList<FileBlockHeader>(); |
||||
fileBlockHeadersByCode.put(fileBlockHeader.getCode(), headers); |
||||
} |
||||
headers.add(fileBlockHeader); |
||||
} |
||||
|
||||
/** |
||||
* @return the block headers |
||||
*/ |
||||
public List<FileBlockHeader> getBlocks() { |
||||
return blocks; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the block header of a given memory address. If the |
||||
* header is not present then null is returned. |
||||
* |
||||
* @param oldMemoryAddress |
||||
* the address of the block header |
||||
* @return loaded header or null if it was not yet loaded |
||||
*/ |
||||
public FileBlockHeader getFileBlock(Long oldMemoryAddress) { |
||||
return fileBlockHeadersByOma.get(oldMemoryAddress); |
||||
} |
||||
|
||||
/** |
||||
* This method returns a list of file blocks' headers of a specified code. |
||||
* |
||||
* @param code |
||||
* the code of file blocks |
||||
* @return a list of file blocks' headers of a specified code |
||||
*/ |
||||
public List<FileBlockHeader> getFileBlocks(BlockCode code) { |
||||
return fileBlockHeadersByCode.get(code); |
||||
} |
||||
|
||||
/** |
||||
* This method adds a helper instance to the helpers' map. |
||||
* |
||||
* @param <T> |
||||
* the type of the helper |
||||
* @param clazz |
||||
* helper's class definition |
||||
* @param helper |
||||
* the helper instance |
||||
*/ |
||||
public <T> void putHelper(Class<T> clazz, AbstractBlenderHelper helper) { |
||||
helpers.put(clazz.getSimpleName(), helper); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
public <T> T getHelper(Class<?> clazz) { |
||||
return (T) helpers.get(clazz.getSimpleName()); |
||||
} |
||||
|
||||
/** |
||||
* This method adds a loaded feature to the map. The key is its unique old |
||||
* memory address. |
||||
* |
||||
* @param oldMemoryAddress |
||||
* the address of the feature |
||||
* @param featureDataType |
||||
* @param feature |
||||
* the feature we want to store |
||||
*/ |
||||
public void addLoadedFeatures(Long oldMemoryAddress, LoadedDataType featureDataType, Object feature) { |
||||
if (oldMemoryAddress == null || featureDataType == null || feature == null) { |
||||
throw new IllegalArgumentException("One of the given arguments is null!"); |
||||
} |
||||
Map<LoadedDataType, Object> map = loadedFeatures.get(oldMemoryAddress); |
||||
if (map == null) { |
||||
map = new HashMap<BlenderContext.LoadedDataType, Object>(); |
||||
loadedFeatures.put(oldMemoryAddress, map); |
||||
} |
||||
map.put(featureDataType, feature); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the feature of a given memory address. If the feature |
||||
* is not yet loaded then null is returned. |
||||
* |
||||
* @param oldMemoryAddress |
||||
* the address of the feature |
||||
* @param loadedFeatureDataType |
||||
* the type of data we want to retrieve it can be either filled |
||||
* structure or already converted feature |
||||
* @return loaded feature or null if it was not yet loaded |
||||
*/ |
||||
public Object getLoadedFeature(Long oldMemoryAddress, LoadedDataType loadedFeatureDataType) { |
||||
Map<LoadedDataType, Object> result = loadedFeatures.get(oldMemoryAddress); |
||||
if (result != null) { |
||||
return result.get(loadedFeatureDataType); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* The method adds linked content to the blender context. |
||||
* @param blenderFilePath |
||||
* the path of linked blender file |
||||
* @param featureGroup |
||||
* the linked feature group (ie. scenes, materials, meshes, etc.) |
||||
* @param feature |
||||
* the linked feature |
||||
*/ |
||||
@Deprecated |
||||
public void addLinkedFeature(String blenderFilePath, String featureGroup, Object feature) { |
||||
// the method is deprecated and empty at the moment
|
||||
} |
||||
|
||||
/** |
||||
* The method returns linked feature of a given name from the specified blender path. |
||||
* @param blenderFilePath |
||||
* the blender file path |
||||
* @param featureName |
||||
* the feature name we want to get |
||||
* @return linked feature or null if none was found |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public Object getLinkedFeature(String blenderFilePath, String featureName) { |
||||
Map<String, Object> linkedFeatures = this.linkedFeatures.get(blenderFilePath); |
||||
if(linkedFeatures != null) { |
||||
String namePrefix = (featureName.charAt(0) + "" + featureName.charAt(1)).toUpperCase(); |
||||
featureName = featureName.substring(2); |
||||
|
||||
if("SC".equals(namePrefix)) { |
||||
List<Node> scenes = (List<Node>) linkedFeatures.get("scenes"); |
||||
if(scenes != null) { |
||||
for(Node scene : scenes) { |
||||
if(featureName.equals(scene.getName())) { |
||||
return scene; |
||||
} |
||||
} |
||||
} |
||||
} else if("OB".equals(namePrefix)) { |
||||
List<Node> features = (List<Node>) linkedFeatures.get("objects"); |
||||
if(features != null) { |
||||
for(Node feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("ME".equals(namePrefix)) { |
||||
List<TemporalMesh> temporalMeshes = (List<TemporalMesh>) linkedFeatures.get("meshes"); |
||||
if(temporalMeshes != null) { |
||||
for(TemporalMesh temporalMesh : temporalMeshes) { |
||||
if(featureName.equals(temporalMesh.getName())) { |
||||
return temporalMesh; |
||||
} |
||||
} |
||||
} |
||||
} else if("MA".equals(namePrefix)) { |
||||
List<MaterialContext> features = (List<MaterialContext>) linkedFeatures.get("materials"); |
||||
if(features != null) { |
||||
for(MaterialContext feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("TX".equals(namePrefix)) { |
||||
List<Texture> features = (List<Texture>) linkedFeatures.get("textures"); |
||||
if(features != null) { |
||||
for(Texture feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("IM".equals(namePrefix)) { |
||||
List<Texture> features = (List<Texture>) linkedFeatures.get("images"); |
||||
if(features != null) { |
||||
for(Texture feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("AC".equals(namePrefix)) { |
||||
List<Animation> features = (List<Animation>) linkedFeatures.get("animations"); |
||||
if(features != null) { |
||||
for(Animation feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("CA".equals(namePrefix)) { |
||||
List<Camera> features = (List<Camera>) linkedFeatures.get("cameras"); |
||||
if(features != null) { |
||||
for(Camera feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("LA".equals(namePrefix)) { |
||||
List<Light> features = (List<Light>) linkedFeatures.get("lights"); |
||||
if(features != null) { |
||||
for(Light feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} else if("FI".equals(featureName)) { |
||||
List<Filter> features = (List<Filter>) linkedFeatures.get("lights"); |
||||
if(features != null) { |
||||
for(Filter feature : features) { |
||||
if(featureName.equals(feature.getName())) { |
||||
return feature; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* @return all linked features for the current blend file |
||||
*/ |
||||
public Map<String, Map<String, Object>> getLinkedFeatures() { |
||||
return linkedFeatures; |
||||
} |
||||
|
||||
/** |
||||
* This method adds the structure to the parent stack. |
||||
* |
||||
* @param parent |
||||
* the structure to be added to the stack |
||||
*/ |
||||
public void pushParent(Structure parent) { |
||||
parentStack.push(parent); |
||||
} |
||||
|
||||
/** |
||||
* This method removes the structure from the top of the parent's stack. |
||||
* |
||||
* @return the structure that was removed from the stack |
||||
*/ |
||||
public Structure popParent() { |
||||
try { |
||||
return parentStack.pop(); |
||||
} catch (EmptyStackException e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method retrieves the structure at the top of the parent's stack but |
||||
* does not remove it. |
||||
* |
||||
* @return the structure from the top of the stack |
||||
*/ |
||||
public Structure peekParent() { |
||||
try { |
||||
return parentStack.peek(); |
||||
} catch (EmptyStackException e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method adds a new modifier to the list. |
||||
* |
||||
* @param ownerOMA |
||||
* the owner's old memory address |
||||
* @param constraints |
||||
* the object's constraints |
||||
*/ |
||||
public void addConstraints(Long ownerOMA, List<Constraint> constraints) { |
||||
List<Constraint> objectConstraints = this.constraints.get(ownerOMA); |
||||
if (objectConstraints == null) { |
||||
objectConstraints = new ArrayList<Constraint>(); |
||||
this.constraints.put(ownerOMA, objectConstraints); |
||||
} |
||||
objectConstraints.addAll(constraints); |
||||
} |
||||
|
||||
/** |
||||
* Returns constraints applied to the feature of the given OMA. |
||||
* @param ownerOMA |
||||
* the constraints' owner OMA |
||||
* @return a list of constraints or <b>null</b> if no constraints are applied to the feature |
||||
*/ |
||||
public List<Constraint> getConstraints(Long ownerOMA) { |
||||
return constraints.get(ownerOMA); |
||||
} |
||||
|
||||
/** |
||||
* @return all available constraints |
||||
*/ |
||||
public List<Constraint> getAllConstraints() { |
||||
List<Constraint> result = new ArrayList<Constraint>(); |
||||
for (Entry<Long, List<Constraint>> entry : constraints.entrySet()) { |
||||
result.addAll(entry.getValue()); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method adds the animation for the specified OMA of its owner. |
||||
* |
||||
* @param ownerOMA |
||||
* the owner's old memory address |
||||
* @param animation |
||||
* the animation for the feature specified by ownerOMA |
||||
*/ |
||||
public void addAnimation(Long ownerOMA, Animation animation) { |
||||
List<Animation> animList = animations.get(ownerOMA); |
||||
if (animList == null) { |
||||
animList = new ArrayList<Animation>(); |
||||
animations.put(ownerOMA, animList); |
||||
} |
||||
animList.add(animation); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the animation data for the specified owner. |
||||
* |
||||
* @param ownerOMA |
||||
* the old memory address of the animation data owner |
||||
* @return the animation or null if none exists |
||||
*/ |
||||
public List<Animation> getAnimations(Long ownerOMA) { |
||||
return animations.get(ownerOMA); |
||||
} |
||||
|
||||
/** |
||||
* This method sets the skeleton for the specified OMA of its owner. |
||||
* |
||||
* @param skeletonOMA |
||||
* the skeleton's old memory address |
||||
* @param skeleton |
||||
* the skeleton specified by the given OMA |
||||
*/ |
||||
public void setSkeleton(Long skeletonOMA, Skeleton skeleton) { |
||||
skeletons.put(skeletonOMA, skeleton); |
||||
} |
||||
|
||||
/** |
||||
* The method stores a binding between the skeleton and the proper armature |
||||
* node. |
||||
* |
||||
* @param skeleton |
||||
* the skeleton |
||||
* @param node |
||||
* the armature node |
||||
*/ |
||||
public void setNodeForSkeleton(Skeleton skeleton, Node node) { |
||||
nodesWithSkeletons.put(skeleton, node); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the armature node that is defined for the skeleton. |
||||
* |
||||
* @param skeleton |
||||
* the skeleton |
||||
* @return the armature node that defines the skeleton in blender |
||||
*/ |
||||
public Node getControlledNode(Skeleton skeleton) { |
||||
return nodesWithSkeletons.get(skeleton); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the skeleton for the specified OMA of its owner. |
||||
* |
||||
* @param skeletonOMA |
||||
* the skeleton's old memory address |
||||
* @return the skeleton specified by the given OMA |
||||
*/ |
||||
public Skeleton getSkeleton(Long skeletonOMA) { |
||||
return skeletons.get(skeletonOMA); |
||||
} |
||||
|
||||
/** |
||||
* This method sets the bone context for the given bone old memory address. |
||||
* If the context is already set it will be replaced. |
||||
* |
||||
* @param boneOMA |
||||
* the bone's old memory address |
||||
* @param boneContext |
||||
* the bones's context |
||||
*/ |
||||
public void setBoneContext(Long boneOMA, BoneContext boneContext) { |
||||
boneContexts.put(boneOMA, boneContext); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the bone context for the given bone old memory |
||||
* address. If no context exists then <b>null</b> is returned. |
||||
* |
||||
* @param boneOMA |
||||
* the bone's old memory address |
||||
* @return bone's context |
||||
*/ |
||||
public BoneContext getBoneContext(Long boneOMA) { |
||||
return boneContexts.get(boneOMA); |
||||
} |
||||
|
||||
/** |
||||
* Returns bone by given name. |
||||
* |
||||
* @param skeletonOMA |
||||
* the OMA of the skeleton where the bone will be searched |
||||
* @param name |
||||
* the name of the bone |
||||
* @return found bone or null if none bone of a given name exists |
||||
*/ |
||||
public BoneContext getBoneByName(Long skeletonOMA, String name) { |
||||
for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) { |
||||
if (entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) { |
||||
Bone bone = entry.getValue().getBone(); |
||||
if (bone != null && name.equals(bone.getName())) { |
||||
return entry.getValue(); |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Returns bone context for the given bone. |
||||
* |
||||
* @param bone |
||||
* the bone |
||||
* @return the bone's bone context |
||||
*/ |
||||
public BoneContext getBoneContext(Bone bone) { |
||||
for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) { |
||||
if (entry.getValue().getBone().getName().equals(bone.getName())) { |
||||
return entry.getValue(); |
||||
} |
||||
} |
||||
throw new IllegalStateException("Cannot find context for bone: " + bone); |
||||
} |
||||
|
||||
/** |
||||
* This metod returns the default material. |
||||
* |
||||
* @return the default material |
||||
*/ |
||||
public synchronized Material getDefaultMaterial() { |
||||
if (blenderKey.getDefaultMaterial() == null) { |
||||
Material defaultMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); |
||||
defaultMaterial.setColor("Color", ColorRGBA.DarkGray); |
||||
blenderKey.setDefaultMaterial(defaultMaterial); |
||||
} |
||||
return blenderKey.getDefaultMaterial(); |
||||
} |
||||
|
||||
/** |
||||
* Adds a custom marker for scene's feature. |
||||
* |
||||
* @param marker |
||||
* the marker name |
||||
* @param feature |
||||
* te scene's feature (can be node, material or texture or |
||||
* anything else) |
||||
* @param markerValue |
||||
* the marker value |
||||
*/ |
||||
public void addMarker(String marker, Object feature, Object markerValue) { |
||||
if (markerValue == null) { |
||||
throw new IllegalArgumentException("The marker's value cannot be null."); |
||||
} |
||||
Map<Object, Object> markersMap = markers.get(marker); |
||||
if (markersMap == null) { |
||||
markersMap = new HashMap<Object, Object>(); |
||||
markers.put(marker, markersMap); |
||||
} |
||||
markersMap.put(feature, markerValue); |
||||
} |
||||
|
||||
/** |
||||
* Returns the marker value. The returned value is null if no marker was |
||||
* defined for the given feature. |
||||
* |
||||
* @param marker |
||||
* the marker name |
||||
* @param feature |
||||
* the scene's feature |
||||
* @return marker value or null if it was not defined |
||||
*/ |
||||
public Object getMarkerValue(String marker, Object feature) { |
||||
Map<Object, Object> markersMap = markers.get(marker); |
||||
return markersMap == null ? null : markersMap.get(feature); |
||||
} |
||||
|
||||
/** |
||||
* Adds blender action to the context. |
||||
* @param action |
||||
* the action loaded from the blend file |
||||
*/ |
||||
public void addAction(BlenderAction action) { |
||||
actions.put(action.getName(), action); |
||||
} |
||||
|
||||
/** |
||||
* @return a map of blender actions; the key is the action name and the value is action itself |
||||
*/ |
||||
public Map<String, BlenderAction> getActions() { |
||||
return actions; |
||||
} |
||||
|
||||
/** |
||||
* This enum defines what loaded data type user wants to retrieve. It can be |
||||
* either filled structure or already converted data. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static enum LoadedDataType { |
||||
STRUCTURE, FEATURE, TEMPORAL_MESH; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return blenderKey == null ? "BlenderContext [key = null]" : "BlenderContext [ key = " + blenderKey.toString() + " ]"; |
||||
} |
||||
} |
@ -0,0 +1,419 @@ |
||||
/* |
||||
* Copyright (c) 2009-2019 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.Animation; |
||||
import com.jme3.asset.AssetInfo; |
||||
import com.jme3.asset.AssetKey; |
||||
import com.jme3.asset.AssetLoader; |
||||
import com.jme3.asset.AssetLocator; |
||||
import com.jme3.asset.AssetManager; |
||||
import com.jme3.asset.BlenderKey; |
||||
import com.jme3.asset.ModelKey; |
||||
import com.jme3.asset.StreamAssetInfo; |
||||
import com.jme3.light.Light; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.post.Filter; |
||||
import com.jme3.renderer.Camera; |
||||
import com.jme3.scene.CameraNode; |
||||
import com.jme3.scene.LightNode; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.animations.AnimationHelper; |
||||
import com.jme3.scene.plugins.blender.cameras.CameraHelper; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.curves.CurvesHelper; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.BlenderInputStream; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.landscape.LandscapeHelper; |
||||
import com.jme3.scene.plugins.blender.lights.LightHelper; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialContext; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.MeshHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.modifiers.ModifierHelper; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
import com.jme3.scene.plugins.blender.particles.ParticlesHelper; |
||||
import com.jme3.scene.plugins.blender.textures.TextureHelper; |
||||
import com.jme3.texture.Texture; |
||||
|
||||
/** |
||||
* This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BlenderLoader implements AssetLoader { |
||||
private static final Logger LOGGER = Logger.getLogger(BlenderLoader.class.getName()); |
||||
|
||||
@Override |
||||
public Spatial load(AssetInfo assetInfo) throws IOException { |
||||
try { |
||||
BlenderContext blenderContext = this.setup(assetInfo); |
||||
|
||||
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); |
||||
animationHelper.loadAnimations(); |
||||
|
||||
BlenderKey blenderKey = blenderContext.getBlenderKey(); |
||||
LoadedFeatures loadedFeatures = new LoadedFeatures(); |
||||
for (FileBlockHeader block : blenderContext.getBlocks()) { |
||||
switch (block.getCode()) { |
||||
case BLOCK_OB00: |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
Node object = (Node) objectHelper.toObject(block.getStructure(blenderContext), blenderContext); |
||||
if (LOGGER.isLoggable(Level.FINE)) { |
||||
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { object.getName(), object.getLocalTranslation().toString(), object.getParent() == null ? "null" : object.getParent().getName() }); |
||||
} |
||||
if (object.getParent() == null) { |
||||
loadedFeatures.objects.add(object); |
||||
} |
||||
if (object instanceof LightNode && ((LightNode) object).getLight() != null) { |
||||
loadedFeatures.lights.add(((LightNode) object).getLight()); |
||||
} else if (object instanceof CameraNode && ((CameraNode) object).getCamera() != null) { |
||||
loadedFeatures.cameras.add(((CameraNode) object).getCamera()); |
||||
} |
||||
break; |
||||
case BLOCK_SC00:// Scene
|
||||
loadedFeatures.sceneBlocks.add(block); |
||||
break; |
||||
case BLOCK_MA00:// Material
|
||||
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); |
||||
MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext); |
||||
loadedFeatures.materials.add(materialContext); |
||||
break; |
||||
case BLOCK_ME00:// Mesh
|
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
TemporalMesh temporalMesh = meshHelper.toTemporalMesh(block.getStructure(blenderContext), blenderContext); |
||||
loadedFeatures.meshes.add(temporalMesh); |
||||
break; |
||||
case BLOCK_IM00:// Image
|
||||
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class); |
||||
Texture image = textureHelper.loadImageAsTexture(block.getStructure(blenderContext), 0, blenderContext); |
||||
if (image != null && image.getImage() != null) {// render results are stored as images but are not being loaded
|
||||
loadedFeatures.images.add(image); |
||||
} |
||||
break; |
||||
case BLOCK_TE00: |
||||
Structure textureStructure = block.getStructure(blenderContext); |
||||
int type = ((Number) textureStructure.getFieldValue("type")).intValue(); |
||||
if (type == TextureHelper.TEX_IMAGE) { |
||||
TextureHelper texHelper = blenderContext.getHelper(TextureHelper.class); |
||||
Texture texture = texHelper.getTexture(textureStructure, null, blenderContext); |
||||
if (texture != null) {// null is returned when texture has no image
|
||||
loadedFeatures.textures.add(texture); |
||||
} |
||||
} else { |
||||
LOGGER.fine("Only image textures can be loaded as unlinked assets. Generated textures will be applied to an existing object."); |
||||
} |
||||
break; |
||||
case BLOCK_WO00:// World
|
||||
LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class); |
||||
Structure worldStructure = block.getStructure(blenderContext); |
||||
|
||||
String worldName = worldStructure.getName(); |
||||
if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) { |
||||
|
||||
Light ambientLight = landscapeHelper.toAmbientLight(worldStructure); |
||||
if (ambientLight != null) { |
||||
loadedFeatures.objects.add(new LightNode(null, ambientLight)); |
||||
loadedFeatures.lights.add(ambientLight); |
||||
} |
||||
loadedFeatures.sky = landscapeHelper.toSky(worldStructure); |
||||
loadedFeatures.backgroundColor = landscapeHelper.toBackgroundColor(worldStructure); |
||||
|
||||
Filter fogFilter = landscapeHelper.toFog(worldStructure); |
||||
if (fogFilter != null) { |
||||
loadedFeatures.filters.add(landscapeHelper.toFog(worldStructure)); |
||||
} |
||||
} |
||||
break; |
||||
case BLOCK_AC00: |
||||
LOGGER.fine("Loading unlinked animations is not yet supported!"); |
||||
break; |
||||
default: |
||||
LOGGER.log(Level.FINEST, "Ommiting the block: {0}.", block.getCode()); |
||||
} |
||||
} |
||||
|
||||
LOGGER.fine("Baking constraints after every feature is loaded."); |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
constraintHelper.bakeConstraints(blenderContext); |
||||
|
||||
LOGGER.fine("Loading scenes and attaching them to the root object."); |
||||
for (FileBlockHeader sceneBlock : loadedFeatures.sceneBlocks) { |
||||
loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext), blenderContext)); |
||||
} |
||||
|
||||
LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene and loaded features to it."); |
||||
Node modelRoot = new Node(blenderKey.getName()); |
||||
for (Node scene : loadedFeatures.scenes) { |
||||
modelRoot.attachChild(scene); |
||||
} |
||||
|
||||
if (blenderKey.isLoadUnlinkedAssets()) { |
||||
LOGGER.fine("Setting loaded content as user data in resulting sptaial."); |
||||
Map<String, Map<String, Object>> linkedData = new HashMap<String, Map<String, Object>>(); |
||||
|
||||
Map<String, Object> thisFileData = new HashMap<String, Object>(); |
||||
thisFileData.put("scenes", loadedFeatures.scenes == null ? new ArrayList<Object>() : loadedFeatures.scenes); |
||||
thisFileData.put("objects", loadedFeatures.objects == null ? new ArrayList<Object>() : loadedFeatures.objects); |
||||
thisFileData.put("meshes", loadedFeatures.meshes == null ? new ArrayList<Object>() : loadedFeatures.meshes); |
||||
thisFileData.put("materials", loadedFeatures.materials == null ? new ArrayList<Object>() : loadedFeatures.materials); |
||||
thisFileData.put("textures", loadedFeatures.textures == null ? new ArrayList<Object>() : loadedFeatures.textures); |
||||
thisFileData.put("images", loadedFeatures.images == null ? new ArrayList<Object>() : loadedFeatures.images); |
||||
thisFileData.put("animations", loadedFeatures.animations == null ? new ArrayList<Object>() : loadedFeatures.animations); |
||||
thisFileData.put("cameras", loadedFeatures.cameras == null ? new ArrayList<Object>() : loadedFeatures.cameras); |
||||
thisFileData.put("lights", loadedFeatures.lights == null ? new ArrayList<Object>() : loadedFeatures.lights); |
||||
thisFileData.put("filters", loadedFeatures.filters == null ? new ArrayList<Object>() : loadedFeatures.filters); |
||||
thisFileData.put("backgroundColor", loadedFeatures.backgroundColor); |
||||
thisFileData.put("sky", loadedFeatures.sky); |
||||
|
||||
linkedData.put("this", thisFileData); |
||||
linkedData.putAll(blenderContext.getLinkedFeatures()); |
||||
|
||||
modelRoot.setUserData("linkedData", linkedData); |
||||
} |
||||
|
||||
return modelRoot; |
||||
} catch (BlenderFileException e) { |
||||
throw new IOException(e.getLocalizedMessage(), e); |
||||
} catch (Exception e) { |
||||
throw new IOException("Unexpected importer exception occurred: " + e.getLocalizedMessage(), e); |
||||
} finally { |
||||
this.clear(assetInfo); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given structure to a scene node. |
||||
* @param structure |
||||
* structure of a scene |
||||
* @param blenderContext the blender context |
||||
* @return scene's node |
||||
* @throws BlenderFileException |
||||
* an exception throw when problems with blender file occur |
||||
*/ |
||||
private Node toScene(Structure structure, BlenderContext blenderContext) throws BlenderFileException { |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
Node result = new Node(structure.getName()); |
||||
List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase(); |
||||
for (Structure b : base) { |
||||
Pointer pObject = (Pointer) b.getFieldValue("object"); |
||||
if (pObject.isNotNull()) { |
||||
Structure objectStructure = pObject.fetchData().get(0); |
||||
|
||||
Object object = objectHelper.toObject(objectStructure, blenderContext); |
||||
if (object instanceof Node) { |
||||
if (LOGGER.isLoggable(Level.FINE)) { |
||||
LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() }); |
||||
} |
||||
|
||||
if (((Node) object).getParent() == null) { |
||||
result.attachChild((Spatial) object); |
||||
} |
||||
|
||||
if(object instanceof LightNode) { |
||||
result.addLight(((LightNode) object).getLight()); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method sets up the loader. |
||||
* @param assetInfo |
||||
* the asset info |
||||
* @throws BlenderFileException |
||||
* an exception is throw when something wrong happens with blender file |
||||
*/ |
||||
protected BlenderContext setup(AssetInfo assetInfo) throws BlenderFileException { |
||||
// registering loaders
|
||||
ModelKey modelKey = (ModelKey) assetInfo.getKey(); |
||||
BlenderKey blenderKey; |
||||
if (modelKey instanceof BlenderKey) { |
||||
blenderKey = (BlenderKey) modelKey; |
||||
} else { |
||||
blenderKey = new BlenderKey(modelKey.getName()); |
||||
} |
||||
|
||||
// opening stream
|
||||
BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream()); |
||||
|
||||
// reading blocks
|
||||
List<FileBlockHeader> blocks = new ArrayList<FileBlockHeader>(); |
||||
FileBlockHeader fileBlock; |
||||
BlenderContext blenderContext = new BlenderContext(); |
||||
blenderContext.setBlenderVersion(inputStream.getVersionNumber()); |
||||
blenderContext.setAssetManager(assetInfo.getManager()); |
||||
blenderContext.setInputStream(inputStream); |
||||
blenderContext.setBlenderKey(blenderKey); |
||||
|
||||
// creating helpers
|
||||
blenderContext.putHelper(AnimationHelper.class, new AnimationHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(TextureHelper.class, new TextureHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(MeshHelper.class, new MeshHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(ObjectHelper.class, new ObjectHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(CurvesHelper.class, new CurvesHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(LightHelper.class, new LightHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(CameraHelper.class, new CameraHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(ModifierHelper.class, new ModifierHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(MaterialHelper.class, new MaterialHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(inputStream.getVersionNumber(), blenderContext)); |
||||
|
||||
// reading the blocks (dna block is automatically saved in the blender context when found)
|
||||
FileBlockHeader sceneFileBlock = null; |
||||
do { |
||||
fileBlock = new FileBlockHeader(inputStream, blenderContext); |
||||
if (!fileBlock.isDnaBlock()) { |
||||
blocks.add(fileBlock); |
||||
// save the scene's file block
|
||||
if (fileBlock.getCode() == BlockCode.BLOCK_SC00) { |
||||
sceneFileBlock = fileBlock; |
||||
} |
||||
} |
||||
} while (!fileBlock.isLastBlock()); |
||||
if (sceneFileBlock != null) { |
||||
blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext)); |
||||
} |
||||
|
||||
// adding locator for linked content
|
||||
assetInfo.getManager().registerLocator(assetInfo.getKey().getName(), LinkedContentLocator.class); |
||||
|
||||
return blenderContext; |
||||
} |
||||
|
||||
/** |
||||
* The internal data is only needed during loading so make it unreachable so that the GC can release |
||||
* that memory (which can be quite large amount). |
||||
*/ |
||||
protected void clear(AssetInfo assetInfo) { |
||||
assetInfo.getManager().unregisterLocator(assetInfo.getKey().getName(), LinkedContentLocator.class); |
||||
} |
||||
|
||||
/** |
||||
* This class holds the loading results according to the given loading flag. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class LoadedFeatures { |
||||
private List<FileBlockHeader> sceneBlocks = new ArrayList<FileBlockHeader>(); |
||||
/** The scenes from the file. */ |
||||
private List<Node> scenes = new ArrayList<Node>(); |
||||
/** Objects from all scenes. */ |
||||
private List<Node> objects = new ArrayList<Node>(); |
||||
/** All meshes. */ |
||||
private List<TemporalMesh> meshes = new ArrayList<TemporalMesh>(); |
||||
/** Materials from all objects. */ |
||||
private List<MaterialContext> materials = new ArrayList<MaterialContext>(); |
||||
/** Textures from all objects. */ |
||||
private List<Texture> textures = new ArrayList<Texture>(); |
||||
/** The images stored in the blender file. */ |
||||
private List<Texture> images = new ArrayList<Texture>(); |
||||
/** Animations of all objects. */ |
||||
private List<Animation> animations = new ArrayList<Animation>(); |
||||
/** All cameras from the file. */ |
||||
private List<Camera> cameras = new ArrayList<Camera>(); |
||||
/** All lights from the file. */ |
||||
private List<Light> lights = new ArrayList<Light>(); |
||||
/** Loaded sky. */ |
||||
private Spatial sky; |
||||
/** Scene filters (ie. FOG). */ |
||||
private List<Filter> filters = new ArrayList<Filter>(); |
||||
/** |
||||
* The background color of the render loaded from the horizon color of the world. If no world is used than the gray color |
||||
* is set to default (as in blender editor. |
||||
*/ |
||||
private ColorRGBA backgroundColor = ColorRGBA.Gray; |
||||
} |
||||
|
||||
public static class LinkedContentLocator implements AssetLocator { |
||||
private File rootFolder; |
||||
|
||||
@Override |
||||
public void setRootPath(String rootPath) { |
||||
rootFolder = new File(rootPath); |
||||
if(rootFolder.isFile()) { |
||||
rootFolder = rootFolder.getParentFile(); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("rawtypes") |
||||
@Override |
||||
public AssetInfo locate(AssetManager manager, AssetKey key) { |
||||
if(key instanceof BlenderKey) { |
||||
File linkedAbsoluteFile = new File(key.getName()); |
||||
if(linkedAbsoluteFile.exists() && linkedAbsoluteFile.isFile()) { |
||||
try { |
||||
return new StreamAssetInfo(manager, key, new FileInputStream(linkedAbsoluteFile)); |
||||
} catch (FileNotFoundException e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
File linkedFileInCurrentAssetFolder = new File(rootFolder, linkedAbsoluteFile.getName()); |
||||
if(linkedFileInCurrentAssetFolder.exists() && linkedFileInCurrentAssetFolder.isFile()) { |
||||
try { |
||||
return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentAssetFolder)); |
||||
} catch (FileNotFoundException e) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
File linkedFileInCurrentFolder = new File(".", linkedAbsoluteFile.getName()); |
||||
if(linkedFileInCurrentFolder.exists() && linkedFileInCurrentFolder.isFile()) { |
||||
try { |
||||
return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentFolder)); |
||||
} catch (FileNotFoundException e) { |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender; |
||||
|
||||
/** |
||||
* This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures. |
||||
* @deprecated this class is deprecated; use BlenderLoader instead |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BlenderModelLoader extends BlenderLoader { |
||||
} |
@ -0,0 +1,391 @@ |
||||
package com.jme3.scene.plugins.blender.animations; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map.Entry; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.AnimControl; |
||||
import com.jme3.animation.Animation; |
||||
import com.jme3.animation.BoneTrack; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.animation.SkeletonControl; |
||||
import com.jme3.animation.SpatialTrack; |
||||
import com.jme3.asset.BlenderKey.AnimationMatchMethod; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.Ipo.ConstIpo; |
||||
import com.jme3.scene.plugins.blender.curves.BezierCurve; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.BlenderInputStream; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
|
||||
/** |
||||
* The helper class that helps in animations loading. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class AnimationHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName()); |
||||
|
||||
public AnimationHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* Loads all animations that are stored in the blender file. The animations are not yet applied to the scene features. |
||||
* This should be called before objects are loaded. |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with blender file reading occur |
||||
*/ |
||||
public void loadAnimations() throws BlenderFileException { |
||||
LOGGER.info("Loading animations that will be later applied to scene features."); |
||||
List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(BlockCode.BLOCK_AC00); |
||||
if (actionHeaders != null) { |
||||
for (FileBlockHeader header : actionHeaders) { |
||||
Structure actionStructure = header.getStructure(blenderContext); |
||||
LOGGER.log(Level.INFO, "Found animation: {0}.", actionStructure.getName()); |
||||
blenderContext.addAction(this.getTracks(actionStructure, blenderContext)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method applies animations to the given node. The names of the animations should be the same as actions names in the blender file. |
||||
* @param node |
||||
* the node to whom the animations will be applied |
||||
* @param animationMatchMethod |
||||
* the way animation should be matched with node |
||||
*/ |
||||
public void applyAnimations(Node node, AnimationMatchMethod animationMatchMethod) { |
||||
List<BlenderAction> actions = this.getActions(node, animationMatchMethod); |
||||
if (actions.size() > 0) { |
||||
List<Animation> animations = new ArrayList<Animation>(); |
||||
for (BlenderAction action : actions) { |
||||
SpatialTrack[] tracks = action.toTracks(node, blenderContext); |
||||
if (tracks != null && tracks.length > 0) { |
||||
Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime()); |
||||
spatialAnimation.setTracks(tracks); |
||||
animations.add(spatialAnimation); |
||||
blenderContext.addAnimation((Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation); |
||||
} |
||||
} |
||||
|
||||
if (animations.size() > 0) { |
||||
AnimControl control = new AnimControl(); |
||||
HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size()); |
||||
for (int i = 0; i < animations.size(); ++i) { |
||||
Animation animation = animations.get(i); |
||||
anims.put(animation.getName(), animation); |
||||
} |
||||
control.setAnimations(anims); |
||||
node.addControl(control); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method applies skeleton animations to the given node. |
||||
* @param node |
||||
* the node where the animations will be applied |
||||
* @param skeleton |
||||
* the skeleton of the node |
||||
* @param animationMatchMethod |
||||
* the way animation should be matched with skeleton |
||||
*/ |
||||
public void applyAnimations(Node node, Skeleton skeleton, AnimationMatchMethod animationMatchMethod) { |
||||
node.addControl(new SkeletonControl(skeleton)); |
||||
blenderContext.setNodeForSkeleton(skeleton, node); |
||||
List<BlenderAction> actions = this.getActions(skeleton, animationMatchMethod); |
||||
|
||||
if (actions.size() > 0) { |
||||
List<Animation> animations = new ArrayList<Animation>(); |
||||
for (BlenderAction action : actions) { |
||||
BoneTrack[] tracks = action.toTracks(skeleton, blenderContext); |
||||
if (tracks != null && tracks.length > 0) { |
||||
Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime()); |
||||
boneAnimation.setTracks(tracks); |
||||
animations.add(boneAnimation); |
||||
Long animatedNodeOMA = ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue(); |
||||
blenderContext.addAnimation(animatedNodeOMA, boneAnimation); |
||||
} |
||||
} |
||||
if (animations.size() > 0) { |
||||
AnimControl control = new AnimControl(skeleton); |
||||
HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size()); |
||||
for (int i = 0; i < animations.size(); ++i) { |
||||
Animation animation = animations.get(i); |
||||
anims.put(animation.getName(), animation); |
||||
} |
||||
control.setAnimations(anims); |
||||
node.addControl(control); |
||||
|
||||
// make sure that SkeletonControl is added AFTER the AnimControl
|
||||
SkeletonControl skeletonControl = node.getControl(SkeletonControl.class); |
||||
if (skeletonControl != null) { |
||||
node.removeControl(SkeletonControl.class); |
||||
node.addControl(skeletonControl); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method creates an ipo object used for interpolation calculations. |
||||
* |
||||
* @param ipoStructure |
||||
* the structure with ipo definition |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the ipo object |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public Ipo fromIpoStructure(Structure ipoStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
Structure curvebase = (Structure) ipoStructure.getFieldValue("curve"); |
||||
|
||||
// preparing bezier curves
|
||||
Ipo result = null; |
||||
List<Structure> curves = curvebase.evaluateListBase();// IpoCurve
|
||||
if (curves.size() > 0) { |
||||
BezierCurve[] bezierCurves = new BezierCurve[curves.size()]; |
||||
int frame = 0; |
||||
for (Structure curve : curves) { |
||||
Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt"); |
||||
List<Structure> bezTriples = pBezTriple.fetchData(); |
||||
int type = ((Number) curve.getFieldValue("adrcode")).intValue(); |
||||
bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2); |
||||
} |
||||
curves.clear(); |
||||
result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion()); |
||||
Long ipoOma = ipoStructure.getOldMemoryAddress(); |
||||
blenderContext.addLoadedFeatures(ipoOma, LoadedDataType.STRUCTURE, ipoStructure); |
||||
blenderContext.addLoadedFeatures(ipoOma, LoadedDataType.FEATURE, result); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method creates an ipo with only a single value. No track type is |
||||
* specified so do not use it for calculating tracks. |
||||
* |
||||
* @param constValue |
||||
* the value of this ipo |
||||
* @return constant ipo |
||||
*/ |
||||
public Ipo fromValue(float constValue) { |
||||
return new ConstIpo(constValue); |
||||
} |
||||
|
||||
/** |
||||
* This method retuns the bone tracks for animation. |
||||
* |
||||
* @param actionStructure |
||||
* the structure containing the tracks |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of tracks for the specified animation |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there are problems with the blend |
||||
* file |
||||
*/ |
||||
private BlenderAction getTracks(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (blenderVersion < 250) { |
||||
return this.getTracks249(actionStructure, blenderContext); |
||||
} else { |
||||
return this.getTracks250(actionStructure, blenderContext); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method retuns the bone tracks for animation for blender version 2.50 |
||||
* and higher. |
||||
* |
||||
* @param actionStructure |
||||
* the structure containing the tracks |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of tracks for the specified animation |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there are problems with the blend |
||||
* file |
||||
*/ |
||||
private BlenderAction getTracks250(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Getting tracks!"); |
||||
Structure groups = (Structure) actionStructure.getFieldValue("groups"); |
||||
List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup
|
||||
BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps()); |
||||
int lastFrame = 1; |
||||
for (Structure actionGroup : actionGroups) { |
||||
String name = actionGroup.getFieldValue("name").toString(); |
||||
List<Structure> channels = ((Structure) actionGroup.getFieldValue("channels")).evaluateListBase(); |
||||
BezierCurve[] bezierCurves = new BezierCurve[channels.size()]; |
||||
int channelCounter = 0; |
||||
for (Structure c : channels) { |
||||
int type = this.getCurveType(c, blenderContext); |
||||
Pointer pBezTriple = (Pointer) c.getFieldValue("bezt"); |
||||
List<Structure> bezTriples = pBezTriple.fetchData(); |
||||
bezierCurves[channelCounter++] = new BezierCurve(type, bezTriples, 2); |
||||
} |
||||
|
||||
Ipo ipo = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion()); |
||||
lastFrame = Math.max(lastFrame, ipo.getLastFrame()); |
||||
blenderAction.featuresTracks.put(name, ipo); |
||||
} |
||||
blenderAction.stopFrame = lastFrame; |
||||
return blenderAction; |
||||
} |
||||
|
||||
/** |
||||
* This method retuns the bone tracks for animation for blender version 2.49 |
||||
* (and probably several lower versions too). |
||||
* |
||||
* @param actionStructure |
||||
* the structure containing the tracks |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of tracks for the specified animation |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there are problems with the blend |
||||
* file |
||||
*/ |
||||
private BlenderAction getTracks249(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Getting tracks!"); |
||||
Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase"); |
||||
List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel
|
||||
BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps()); |
||||
int lastFrame = 1; |
||||
for (Structure bActionChannel : actionChannels) { |
||||
String animatedFeatureName = bActionChannel.getFieldValue("name").toString(); |
||||
Pointer p = (Pointer) bActionChannel.getFieldValue("ipo"); |
||||
if (!p.isNull()) { |
||||
Structure ipoStructure = p.fetchData().get(0); |
||||
Ipo ipo = this.fromIpoStructure(ipoStructure, blenderContext); |
||||
if (ipo != null) {// this can happen when ipo with no curves appear in blender file
|
||||
lastFrame = Math.max(lastFrame, ipo.getLastFrame()); |
||||
blenderAction.featuresTracks.put(animatedFeatureName, ipo); |
||||
} |
||||
} |
||||
} |
||||
blenderAction.stopFrame = lastFrame; |
||||
return blenderAction; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the type of the ipo curve. |
||||
* |
||||
* @param structure |
||||
* the structure must contain the 'rna_path' field and |
||||
* 'array_index' field (the type is not important here) |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the type of the curve |
||||
*/ |
||||
public int getCurveType(Structure structure, BlenderContext blenderContext) { |
||||
// reading rna path first
|
||||
BlenderInputStream bis = blenderContext.getInputStream(); |
||||
int currentPosition = bis.getPosition(); |
||||
Pointer pRnaPath = (Pointer) structure.getFieldValue("rna_path"); |
||||
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pRnaPath.getOldMemoryAddress()); |
||||
bis.setPosition(dataFileBlock.getBlockPosition()); |
||||
String rnaPath = bis.readString(); |
||||
bis.setPosition(currentPosition); |
||||
int arrayIndex = ((Number) structure.getFieldValue("array_index")).intValue(); |
||||
|
||||
// determining the curve type
|
||||
if (rnaPath.endsWith("location")) { |
||||
return Ipo.AC_LOC_X + arrayIndex; |
||||
} |
||||
if (rnaPath.endsWith("rotation_quaternion")) { |
||||
return Ipo.AC_QUAT_W + arrayIndex; |
||||
} |
||||
if (rnaPath.endsWith("scale")) { |
||||
return Ipo.AC_SIZE_X + arrayIndex; |
||||
} |
||||
if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) { |
||||
return Ipo.OB_ROT_X + arrayIndex; |
||||
} |
||||
LOGGER.log(Level.WARNING, "Unknown curve rna path: {0}", rnaPath); |
||||
return -1; |
||||
} |
||||
|
||||
/** |
||||
* The method returns the actions for the given skeleton. The actions represent armature animation in blender. |
||||
* @param skeleton |
||||
* the skeleton we fetch the actions for |
||||
* @param animationMatchMethod |
||||
* the method of animation matching |
||||
* @return a list of animations for the specified skeleton |
||||
*/ |
||||
private List<BlenderAction> getActions(Skeleton skeleton, AnimationMatchMethod animationMatchMethod) { |
||||
List<BlenderAction> result = new ArrayList<BlenderAction>(); |
||||
|
||||
// first get a set of bone names
|
||||
Set<String> boneNames = new HashSet<String>(); |
||||
for (int i = 0; i < skeleton.getBoneCount(); ++i) { |
||||
String boneName = skeleton.getBone(i).getName(); |
||||
if (boneName != null && boneName.length() > 0) { |
||||
boneNames.add(skeleton.getBone(i).getName()); |
||||
} |
||||
} |
||||
|
||||
// finding matches
|
||||
Set<String> matchingNames = new HashSet<String>(); |
||||
for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) { |
||||
// compute how many action tracks match the skeleton bones' names
|
||||
for (String boneName : boneNames) { |
||||
if (actionEntry.getValue().hasTrackName(boneName)) { |
||||
matchingNames.add(boneName); |
||||
} |
||||
} |
||||
|
||||
BlenderAction action = null; |
||||
if (animationMatchMethod == AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH && matchingNames.size() > 0) { |
||||
action = actionEntry.getValue(); |
||||
} else if (matchingNames.size() == actionEntry.getValue().getTracksCount()) { |
||||
action = actionEntry.getValue(); |
||||
} |
||||
|
||||
if (action != null) { |
||||
// remove the tracks that do not match the bone names if the matching method is different from ALL_NAMES_MATCH
|
||||
if (animationMatchMethod != AnimationMatchMethod.ALL_NAMES_MATCH) { |
||||
action = action.clone(); |
||||
action.removeTracksThatAreNotInTheCollection(matchingNames); |
||||
} |
||||
result.add(action); |
||||
} |
||||
|
||||
matchingNames.clear(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method returns the actions for the given node. The actions represent object animation in blender. |
||||
* @param node |
||||
* the node we fetch the actions for |
||||
* @param animationMatchMethod |
||||
* the method of animation matching |
||||
* @return a list of animations for the specified node |
||||
*/ |
||||
private List<BlenderAction> getActions(Node node, AnimationMatchMethod animationMatchMethod) { |
||||
List<BlenderAction> result = new ArrayList<BlenderAction>(); |
||||
|
||||
for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) { |
||||
if (actionEntry.getValue().hasTrackName(node.getName())) { |
||||
result.add(actionEntry.getValue()); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,134 @@ |
||||
package com.jme3.scene.plugins.blender.animations; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
|
||||
import com.jme3.animation.BoneTrack; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.animation.SpatialTrack; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
|
||||
/** |
||||
* An abstract representation of animation. The data stored here is mainly a |
||||
* raw action data loaded from blender. It can later be transformed into |
||||
* bone or spatial animation and applied to the specified node. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BlenderAction implements Cloneable { |
||||
/** The action name. */ |
||||
/* package */final String name; |
||||
/** Animation speed - frames per second. */ |
||||
/* package */int fps; |
||||
/** |
||||
* The last frame of the animation (the last ipo curve node position is |
||||
* used as a last frame). |
||||
*/ |
||||
/* package */int stopFrame; |
||||
/** |
||||
* Tracks of the features. In case of bone animation the keys are the |
||||
* names of the bones. In case of spatial animation - the node's name is |
||||
* used. A single ipo contains all tracks for location, rotation and |
||||
* scales. |
||||
*/ |
||||
/* package */Map<String, Ipo> featuresTracks = new HashMap<String, Ipo>(); |
||||
|
||||
public BlenderAction(String name, int fps) { |
||||
this.name = name; |
||||
this.fps = fps; |
||||
} |
||||
|
||||
public void removeTracksThatAreNotInTheCollection(Collection<String> trackNames) { |
||||
Map<String, Ipo> newTracks = new HashMap<String, Ipo>(); |
||||
for (String trackName : trackNames) { |
||||
if (featuresTracks.containsKey(trackName)) { |
||||
newTracks.put(trackName, featuresTracks.get(trackName)); |
||||
} |
||||
} |
||||
featuresTracks = newTracks; |
||||
} |
||||
|
||||
@Override |
||||
public BlenderAction clone() { |
||||
BlenderAction result = new BlenderAction(name, fps); |
||||
result.stopFrame = stopFrame; |
||||
result.featuresTracks = new HashMap<String, Ipo>(featuresTracks); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Converts the action into JME spatial animation tracks. |
||||
* |
||||
* @param node |
||||
* the node that will be animated |
||||
* @return the spatial tracks for the node |
||||
*/ |
||||
public SpatialTrack[] toTracks(Node node, BlenderContext blenderContext) { |
||||
List<SpatialTrack> tracks = new ArrayList<SpatialTrack>(featuresTracks.size()); |
||||
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) { |
||||
tracks.add((SpatialTrack) entry.getValue().calculateTrack(0, null, node.getLocalTranslation(), node.getLocalRotation(), node.getLocalScale(), 1, stopFrame, fps, true)); |
||||
} |
||||
return tracks.toArray(new SpatialTrack[tracks.size()]); |
||||
} |
||||
|
||||
/** |
||||
* Converts the action into JME bone animation tracks. |
||||
* |
||||
* @param skeleton |
||||
* the skeleton that will be animated |
||||
* @return the bone tracks for the node |
||||
*/ |
||||
public BoneTrack[] toTracks(Skeleton skeleton, BlenderContext blenderContext) { |
||||
List<BoneTrack> tracks = new ArrayList<BoneTrack>(featuresTracks.size()); |
||||
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) { |
||||
int boneIndex = skeleton.getBoneIndex(entry.getKey()); |
||||
BoneContext boneContext = blenderContext.getBoneContext(skeleton.getBone(boneIndex)); |
||||
tracks.add((BoneTrack) entry.getValue().calculateTrack(boneIndex, boneContext, boneContext.getBone().getBindPosition(), boneContext.getBone().getBindRotation(), boneContext.getBone().getBindScale(), 1, stopFrame, fps, false)); |
||||
} |
||||
return tracks.toArray(new BoneTrack[tracks.size()]); |
||||
} |
||||
|
||||
/** |
||||
* @return the name of the action |
||||
*/ |
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
/** |
||||
* @return the time of animations (in seconds) |
||||
*/ |
||||
public float getAnimationTime() { |
||||
return (stopFrame - 1) / (float) fps; |
||||
} |
||||
|
||||
/** |
||||
* Determines if the current action has a track of a given name. |
||||
* CAUTION! The names are case sensitive. |
||||
* |
||||
* @param name |
||||
* the name of the track |
||||
* @return <B>true</b> if the track of a given name exists for the |
||||
* action and <b>false</b> otherwise |
||||
*/ |
||||
public boolean hasTrackName(String name) { |
||||
return featuresTracks.containsKey(name); |
||||
} |
||||
|
||||
/** |
||||
* @return the amount of tracks in current action |
||||
*/ |
||||
public int getTracksCount() { |
||||
return featuresTracks.size(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "BlenderTrack [name = " + name + "; tracks = [" + featuresTracks.keySet() + "]]"; |
||||
} |
||||
} |
@ -0,0 +1,400 @@ |
||||
package com.jme3.scene.plugins.blender.animations; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
|
||||
/** |
||||
* This class holds the basic data that describes a bone. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BoneContext { |
||||
// the flags of the bone
|
||||
public static final int SELECTED = 0x000001; |
||||
public static final int CONNECTED_TO_PARENT = 0x000010; |
||||
public static final int DEFORM = 0x001000; |
||||
public static final int NO_LOCAL_LOCATION = 0x400000; |
||||
public static final int NO_INHERIT_SCALE = 0x008000; |
||||
public static final int NO_INHERIT_ROTATION = 0x000200; |
||||
|
||||
/** |
||||
* The bones' matrices have, unlike objects', the coordinate system identical to JME's (Y axis is UP, X to the right and Z toward us). |
||||
* So in order to have them loaded properly we need to transform their armature matrix (which blender sees as rotated) to make sure we get identical results. |
||||
*/ |
||||
public static final Matrix4f BONE_ARMATURE_TRANSFORMATION_MATRIX = new Matrix4f(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1); |
||||
|
||||
private static final int IKFLAG_LOCK_X = 0x01; |
||||
private static final int IKFLAG_LOCK_Y = 0x02; |
||||
private static final int IKFLAG_LOCK_Z = 0x04; |
||||
private static final int IKFLAG_LIMIT_X = 0x08; |
||||
private static final int IKFLAG_LIMIT_Y = 0x10; |
||||
private static final int IKFLAG_LIMIT_Z = 0x20; |
||||
|
||||
private BlenderContext blenderContext; |
||||
/** The OMA of the bone's armature object. */ |
||||
private Long armatureObjectOMA; |
||||
/** The OMA of the model that owns the bone's skeleton. */ |
||||
private Long skeletonOwnerOma; |
||||
/** The structure of the bone. */ |
||||
private Structure boneStructure; |
||||
/** Bone's name. */ |
||||
private String boneName; |
||||
/** The bone's flag. */ |
||||
private int flag; |
||||
/** The bone's matrix in world space. */ |
||||
private Matrix4f globalBoneMatrix; |
||||
/** The bone's matrix in the model space. */ |
||||
private Matrix4f boneMatrixInModelSpace; |
||||
/** The parent context. */ |
||||
private BoneContext parent; |
||||
/** The children of this context. */ |
||||
private List<BoneContext> children = new ArrayList<BoneContext>(); |
||||
/** Created bone (available after calling 'buildBone' method). */ |
||||
private Bone bone; |
||||
/** The length of the bone. */ |
||||
private float length; |
||||
/** The bone's deform envelope. */ |
||||
private BoneEnvelope boneEnvelope; |
||||
|
||||
// The below data is used only for IK constraint computations.
|
||||
|
||||
/** The bone's stretch value. */ |
||||
private float ikStretch; |
||||
/** Bone's rotation minimum values. */ |
||||
private Vector3f limitMin; |
||||
/** Bone's rotation maximum values. */ |
||||
private Vector3f limitMax; |
||||
/** The bone's stiffness values (how much it rotates during IK computations. */ |
||||
private Vector3f stiffness; |
||||
/** Values that indicate if any axis' rotation should be limited by some angle. */ |
||||
private boolean[] limits; |
||||
/** Values that indicate if any axis' rotation should be disabled during IK computations. */ |
||||
private boolean[] locks; |
||||
|
||||
/** |
||||
* Constructor. Creates the basic set of bone's data. |
||||
* |
||||
* @param armatureObjectOMA |
||||
* the OMA of the bone's armature object |
||||
* @param boneStructure |
||||
* the bone's structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problem with blender data reading |
||||
* occurs |
||||
*/ |
||||
public BoneContext(Long armatureObjectOMA, Structure boneStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
this(boneStructure, armatureObjectOMA, null, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Creates the basic set of bone's data. |
||||
* |
||||
* @param boneStructure |
||||
* the bone's structure |
||||
* @param armatureObjectOMA |
||||
* the OMA of the bone's armature object |
||||
* @param parent |
||||
* bone's parent (null if the bone is the root bone) |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problem with blender data reading |
||||
* occurs |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private BoneContext(Structure boneStructure, Long armatureObjectOMA, BoneContext parent, BlenderContext blenderContext) throws BlenderFileException { |
||||
this.parent = parent; |
||||
this.blenderContext = blenderContext; |
||||
this.boneStructure = boneStructure; |
||||
this.armatureObjectOMA = armatureObjectOMA; |
||||
boneName = boneStructure.getFieldValue("name").toString(); |
||||
flag = ((Number) boneStructure.getFieldValue("flag")).intValue(); |
||||
length = ((Number) boneStructure.getFieldValue("length")).floatValue(); |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
|
||||
// first get the bone matrix in its armature space
|
||||
globalBoneMatrix = objectHelper.getMatrix(boneStructure, "arm_mat", blenderContext.getBlenderKey().isFixUpAxis()); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
// then make sure it is rotated in a proper way to fit the jme bone transformation conventions
|
||||
globalBoneMatrix.multLocal(BONE_ARMATURE_TRANSFORMATION_MATRIX); |
||||
} |
||||
|
||||
Structure armatureStructure = blenderContext.getFileBlock(armatureObjectOMA).getStructure(blenderContext); |
||||
Spatial armature = (Spatial) objectHelper.toObject(armatureStructure, blenderContext); |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
Matrix4f armatureWorldMatrix = constraintHelper.toMatrix(armature.getWorldTransform(), new Matrix4f()); |
||||
|
||||
// and now compute the final bone matrix in world space
|
||||
globalBoneMatrix = armatureWorldMatrix.mult(globalBoneMatrix); |
||||
|
||||
// load the bone deformation envelope if necessary
|
||||
if ((flag & DEFORM) == 0) {// if the flag is NOT set then the DEFORM is in use
|
||||
boneEnvelope = new BoneEnvelope(boneStructure, armatureWorldMatrix, blenderContext.getBlenderKey().isFixUpAxis()); |
||||
} |
||||
|
||||
// load bone's pose channel data
|
||||
Pointer pPose = (Pointer) armatureStructure.getFieldValue("pose"); |
||||
if (pPose != null && pPose.isNotNull()) { |
||||
List<Structure> poseChannels = ((Structure) pPose.fetchData().get(0).getFieldValue("chanbase")).evaluateListBase(); |
||||
for (Structure poseChannel : poseChannels) { |
||||
Long boneOMA = ((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress(); |
||||
if (boneOMA.equals(this.boneStructure.getOldMemoryAddress())) { |
||||
ikStretch = ((Number) poseChannel.getFieldValue("ikstretch")).floatValue(); |
||||
DynamicArray<Number> limitMin = (DynamicArray<Number>) poseChannel.getFieldValue("limitmin"); |
||||
this.limitMin = new Vector3f(limitMin.get(0).floatValue(), limitMin.get(1).floatValue(), limitMin.get(2).floatValue()); |
||||
|
||||
DynamicArray<Number> limitMax = (DynamicArray<Number>) poseChannel.getFieldValue("limitmax"); |
||||
this.limitMax = new Vector3f(limitMax.get(0).floatValue(), limitMax.get(1).floatValue(), limitMax.get(2).floatValue()); |
||||
|
||||
DynamicArray<Number> stiffness = (DynamicArray<Number>) poseChannel.getFieldValue("stiffness"); |
||||
this.stiffness = new Vector3f(stiffness.get(0).floatValue(), stiffness.get(1).floatValue(), stiffness.get(2).floatValue()); |
||||
|
||||
int ikFlag = ((Number) poseChannel.getFieldValue("ikflag")).intValue(); |
||||
locks = new boolean[] { (ikFlag & IKFLAG_LOCK_X) != 0, (ikFlag & IKFLAG_LOCK_Y) != 0, (ikFlag & IKFLAG_LOCK_Z) != 0 }; |
||||
// limits are enabled when locks are disabled, so we ween to take that into account here
|
||||
limits = new boolean[] { (ikFlag & IKFLAG_LIMIT_X & ~IKFLAG_LOCK_X) != 0, (ikFlag & IKFLAG_LIMIT_Y & ~IKFLAG_LOCK_Y) != 0, (ikFlag & IKFLAG_LIMIT_Z & ~IKFLAG_LOCK_Z) != 0 }; |
||||
break;// we have found what we need, no need to search further
|
||||
} |
||||
} |
||||
} |
||||
|
||||
// create the children
|
||||
List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase(); |
||||
for (Structure child : childbase) { |
||||
children.add(new BoneContext(child, armatureObjectOMA, this, blenderContext)); |
||||
} |
||||
|
||||
blenderContext.setBoneContext(boneStructure.getOldMemoryAddress(), this); |
||||
} |
||||
|
||||
/** |
||||
* This method builds the bone. It recursively builds the bone's children. |
||||
* |
||||
* @param bones |
||||
* a list of bones where the newly created bone will be added |
||||
* @param skeletonOwnerOma |
||||
* the spatial of the object that will own the skeleton |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return newly created bone |
||||
*/ |
||||
public Bone buildBone(List<Bone> bones, Long skeletonOwnerOma, BlenderContext blenderContext) { |
||||
this.skeletonOwnerOma = skeletonOwnerOma; |
||||
Long boneOMA = boneStructure.getOldMemoryAddress(); |
||||
bone = new Bone(boneName); |
||||
bones.add(bone); |
||||
blenderContext.addLoadedFeatures(boneOMA, LoadedDataType.STRUCTURE, boneStructure); |
||||
blenderContext.addLoadedFeatures(boneOMA, LoadedDataType.FEATURE, bone); |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
|
||||
Structure skeletonOwnerObjectStructure = (Structure) blenderContext.getLoadedFeature(skeletonOwnerOma, LoadedDataType.STRUCTURE); |
||||
// I could load 'imat' here, but apparently in some older blenders there were bugs or unfinished functionalities that stored ZERO matrix in imat field
|
||||
// loading 'obmat' and inverting it makes us avoid errors in such cases
|
||||
Matrix4f invertedObjectOwnerGlobalMatrix = objectHelper.getMatrix(skeletonOwnerObjectStructure, "obmat", blenderContext.getBlenderKey().isFixUpAxis()).invertLocal(); |
||||
if (objectHelper.isParent(skeletonOwnerOma, armatureObjectOMA)) { |
||||
boneMatrixInModelSpace = globalBoneMatrix.mult(invertedObjectOwnerGlobalMatrix); |
||||
} else { |
||||
boneMatrixInModelSpace = invertedObjectOwnerGlobalMatrix.mult(globalBoneMatrix); |
||||
} |
||||
|
||||
Matrix4f boneLocalMatrix = parent == null ? boneMatrixInModelSpace : parent.boneMatrixInModelSpace.invert().multLocal(boneMatrixInModelSpace); |
||||
|
||||
Vector3f poseLocation = parent == null || !this.is(CONNECTED_TO_PARENT) ? boneLocalMatrix.toTranslationVector() : new Vector3f(0, parent.length, 0); |
||||
Quaternion rotation = boneLocalMatrix.toRotationQuat().normalizeLocal(); |
||||
Vector3f scale = boneLocalMatrix.toScaleVector(); |
||||
|
||||
bone.setBindTransforms(poseLocation, rotation, scale); |
||||
for (BoneContext child : children) { |
||||
bone.addChild(child.buildBone(bones, skeletonOwnerOma, blenderContext)); |
||||
} |
||||
|
||||
return bone; |
||||
} |
||||
|
||||
/** |
||||
* @return built bone (available after calling 'buildBone' method) |
||||
*/ |
||||
public Bone getBone() { |
||||
return bone; |
||||
} |
||||
|
||||
/** |
||||
* @return the old memory address of the bone |
||||
*/ |
||||
public Long getBoneOma() { |
||||
return boneStructure.getOldMemoryAddress(); |
||||
} |
||||
|
||||
/** |
||||
* The method returns the length of the bone. |
||||
* If you want to use it for bone debugger take model space scale into account and do |
||||
* something like this: |
||||
* <b>boneContext.getLength() * boneContext.getBone().getModelSpaceScale().y</b>. |
||||
* Otherwise the bones might not look as they should in the bone debugger. |
||||
* @return the length of the bone |
||||
*/ |
||||
public float getLength() { |
||||
return length; |
||||
} |
||||
|
||||
/** |
||||
* @return OMA of the bone's armature object |
||||
*/ |
||||
public Long getArmatureObjectOMA() { |
||||
return armatureObjectOMA; |
||||
} |
||||
|
||||
/** |
||||
* @return the OMA of the model that owns the bone's skeleton |
||||
*/ |
||||
public Long getSkeletonOwnerOma() { |
||||
return skeletonOwnerOma; |
||||
} |
||||
|
||||
/** |
||||
* @return the skeleton the bone of this context belongs to |
||||
*/ |
||||
public Skeleton getSkeleton() { |
||||
return blenderContext.getSkeleton(armatureObjectOMA); |
||||
} |
||||
|
||||
/** |
||||
* @return the initial bone's matrix in model space |
||||
*/ |
||||
public Matrix4f getBoneMatrixInModelSpace() { |
||||
return boneMatrixInModelSpace; |
||||
} |
||||
|
||||
/** |
||||
* @return the vertex assigning envelope of the bone |
||||
*/ |
||||
public BoneEnvelope getBoneEnvelope() { |
||||
return boneEnvelope; |
||||
} |
||||
|
||||
/** |
||||
* @return bone's stretch factor |
||||
*/ |
||||
public float getIkStretch() { |
||||
return ikStretch; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the X rotation should be limited |
||||
*/ |
||||
public boolean isLimitX() { |
||||
return limits != null ? limits[0] : false; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the Y rotation should be limited |
||||
*/ |
||||
public boolean isLimitY() { |
||||
return limits != null ? limits[1] : false; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the Z rotation should be limited |
||||
*/ |
||||
public boolean isLimitZ() { |
||||
return limits != null ? limits[2] : false; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the X rotation should be disabled |
||||
*/ |
||||
public boolean isLockX() { |
||||
return locks != null ? locks[0] : false; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the Y rotation should be disabled |
||||
*/ |
||||
public boolean isLockY() { |
||||
return locks != null ? locks[1] : false; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the Z rotation should be disabled |
||||
*/ |
||||
public boolean isLockZ() { |
||||
return locks != null ? locks[2] : false; |
||||
} |
||||
|
||||
/** |
||||
* @return the minimum values in rotation limitation (if limitation is enabled for specific axis). |
||||
*/ |
||||
public Vector3f getLimitMin() { |
||||
return limitMin; |
||||
} |
||||
|
||||
/** |
||||
* @return the maximum values in rotation limitation (if limitation is enabled for specific axis). |
||||
*/ |
||||
public Vector3f getLimitMax() { |
||||
return limitMax; |
||||
} |
||||
|
||||
/** |
||||
* @return the stiffness of the bone |
||||
*/ |
||||
public Vector3f getStiffness() { |
||||
return stiffness; |
||||
} |
||||
|
||||
/** |
||||
* Tells if the bone is of specified property defined by its flag. |
||||
* @param flagMask |
||||
* the mask of the flag (constants defined in this class) |
||||
* @return <b>true</b> if the bone IS of specified proeprty and <b>false</b> otherwise |
||||
*/ |
||||
public boolean is(int flagMask) { |
||||
return (flag & flagMask) != 0; |
||||
} |
||||
|
||||
/** |
||||
* @return the root bone context of this bone context |
||||
*/ |
||||
public BoneContext getRoot() { |
||||
BoneContext result = this; |
||||
while (result.parent != null) { |
||||
result = result.parent; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return a number of bones from this bone to its root |
||||
*/ |
||||
public int getDistanceFromRoot() { |
||||
int result = 0; |
||||
BoneContext boneContext = this; |
||||
while (boneContext.parent != null) { |
||||
boneContext = boneContext.parent; |
||||
++result; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "BoneContext: " + boneName; |
||||
} |
||||
} |
@ -0,0 +1,133 @@ |
||||
package com.jme3.scene.plugins.blender.animations; |
||||
|
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* An implementation of bone envelope. Used when assigning bones to the mesh by envelopes. |
||||
* |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class BoneEnvelope { |
||||
/** A defined distance that will be included in the envelope space. */ |
||||
private float distance; |
||||
/** The bone's weight. */ |
||||
private float weight; |
||||
/** The radius of the bone's head. */ |
||||
private float boneHeadRadius; |
||||
/** The radius of the bone's tail. */ |
||||
private float boneTailRadius; |
||||
/** Head position in rest pose in world space. */ |
||||
private Vector3f head; |
||||
/** Tail position in rest pose in world space. */ |
||||
private Vector3f tail; |
||||
|
||||
/** |
||||
* The constructor of bone envelope. It reads all the needed data. Take notice that the positions of head and tail |
||||
* are computed in the world space and that the points' positions given for computations should be in world space as well. |
||||
* |
||||
* @param boneStructure |
||||
* the blender bone structure |
||||
* @param armatureWorldMatrix |
||||
* the world matrix of the armature object |
||||
* @param fixUpAxis |
||||
* a variable that tells if we use the Y-is up axis orientation |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public BoneEnvelope(Structure boneStructure, Matrix4f armatureWorldMatrix, boolean fixUpAxis) { |
||||
distance = ((Number) boneStructure.getFieldValue("dist")).floatValue(); |
||||
weight = ((Number) boneStructure.getFieldValue("weight")).floatValue(); |
||||
boneHeadRadius = ((Number) boneStructure.getFieldValue("rad_head")).floatValue(); |
||||
boneTailRadius = ((Number) boneStructure.getFieldValue("rad_tail")).floatValue(); |
||||
|
||||
DynamicArray<Number> headArray = (DynamicArray<Number>) boneStructure.getFieldValue("arm_head"); |
||||
head = new Vector3f(headArray.get(0).floatValue(), headArray.get(1).floatValue(), headArray.get(2).floatValue()); |
||||
if (fixUpAxis) { |
||||
float z = head.z; |
||||
head.z = -head.y; |
||||
head.y = z; |
||||
} |
||||
armatureWorldMatrix.mult(head, head);// move the head point to global space
|
||||
|
||||
DynamicArray<Number> tailArray = (DynamicArray<Number>) boneStructure.getFieldValue("arm_tail"); |
||||
tail = new Vector3f(tailArray.get(0).floatValue(), tailArray.get(1).floatValue(), tailArray.get(2).floatValue()); |
||||
if (fixUpAxis) { |
||||
float z = tail.z; |
||||
tail.z = -tail.y; |
||||
tail.y = z; |
||||
} |
||||
armatureWorldMatrix.mult(tail, tail);// move the tail point to global space
|
||||
} |
||||
|
||||
/** |
||||
* The method verifies if the given point is inside the envelope. |
||||
* @param point |
||||
* the point in 3D space (MUST be in a world coordinate space) |
||||
* @return <b>true</b> if the point is inside the envelope and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isInEnvelope(Vector3f point) { |
||||
Vector3f v = tail.subtract(head); |
||||
float boneLength = v.length(); |
||||
v.normalizeLocal(); |
||||
|
||||
// computing a plane that contains 'point' and v is its normal vector
|
||||
// the plane's equation is: Ax + By + Cz + D = 0, where v = [A, B, C]
|
||||
float D = -v.dot(point); |
||||
|
||||
// computing a point where a line that contains head and tail crosses the plane
|
||||
float temp = -(v.dot(head) + D) / v.dot(v); |
||||
Vector3f p = head.add(v.x * temp, v.y * temp, v.z * temp); |
||||
|
||||
// determining if the point p is on the same or other side of head than the tail point
|
||||
Vector3f headToPointOnLineVector = p.subtract(head); |
||||
float headToPointLength = headToPointOnLineVector.length(); |
||||
float cosinus = headToPointOnLineVector.dot(v) / headToPointLength;// the length of v is already = 1; cosinus should be either 1, 0 or -1
|
||||
if (cosinus < 0 && headToPointLength > boneHeadRadius || headToPointLength > boneLength + boneTailRadius) { |
||||
return false;// the point is outside the anvelope
|
||||
} |
||||
|
||||
// now check if the point is inside and envelope
|
||||
float pointDistanceFromLine = point.subtract(p).length(), maximumDistance = 0; |
||||
if (cosinus < 0) { |
||||
// checking if the distance from p to point is inside the half sphere defined by head envelope
|
||||
// compute the distance from the line to the half sphere border
|
||||
maximumDistance = boneHeadRadius; |
||||
} else if (headToPointLength < boneLength) { |
||||
// compute the maximum available distance
|
||||
if (boneTailRadius > boneHeadRadius) { |
||||
// compute the distance from head to p
|
||||
float headToPDistance = p.subtract(head).length(); |
||||
// from tangens function we have
|
||||
float x = headToPDistance * ((boneTailRadius - boneHeadRadius) / boneLength); |
||||
maximumDistance = x + boneHeadRadius; |
||||
} else if (boneTailRadius < boneHeadRadius) { |
||||
// compute the distance from head to p
|
||||
float tailToPDistance = p.subtract(tail).length(); |
||||
// from tangens function we have
|
||||
float x = tailToPDistance * ((boneHeadRadius - boneTailRadius) / boneLength); |
||||
maximumDistance = x + boneTailRadius; |
||||
} else { |
||||
maximumDistance = boneTailRadius; |
||||
} |
||||
} else { |
||||
// checking if the distance from p to point is inside the half sphere defined by tail envelope
|
||||
maximumDistance = boneTailRadius; |
||||
} |
||||
|
||||
return pointDistanceFromLine <= maximumDistance + distance; |
||||
} |
||||
|
||||
/** |
||||
* @return the weight of the bone |
||||
*/ |
||||
public float getWeight() { |
||||
return weight; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "BoneEnvelope [d=" + distance + ", w=" + weight + ", hr=" + boneHeadRadius + ", tr=" + boneTailRadius + ", (" + head + ") -> (" + tail + ")]"; |
||||
} |
||||
} |
@ -0,0 +1,317 @@ |
||||
package com.jme3.scene.plugins.blender.animations; |
||||
|
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.BoneTrack; |
||||
import com.jme3.animation.SpatialTrack; |
||||
import com.jme3.animation.Track; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.curves.BezierCurve; |
||||
|
||||
/** |
||||
* This class is used to calculate bezier curves value for the given frames. The |
||||
* Ipo (interpolation object) consists of several b-spline curves (connected 3rd |
||||
* degree bezier curves) of a different type. |
||||
* |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class Ipo { |
||||
private static final Logger LOGGER = Logger.getLogger(Ipo.class.getName()); |
||||
|
||||
public static final int AC_LOC_X = 1; |
||||
public static final int AC_LOC_Y = 2; |
||||
public static final int AC_LOC_Z = 3; |
||||
public static final int OB_ROT_X = 7; |
||||
public static final int OB_ROT_Y = 8; |
||||
public static final int OB_ROT_Z = 9; |
||||
public static final int AC_SIZE_X = 13; |
||||
public static final int AC_SIZE_Y = 14; |
||||
public static final int AC_SIZE_Z = 15; |
||||
public static final int AC_QUAT_W = 25; |
||||
public static final int AC_QUAT_X = 26; |
||||
public static final int AC_QUAT_Y = 27; |
||||
public static final int AC_QUAT_Z = 28; |
||||
|
||||
/** A list of bezier curves for this interpolation object. */ |
||||
private BezierCurve[] bezierCurves; |
||||
/** Each ipo contains one bone track. */ |
||||
private Track calculatedTrack; |
||||
/** This variable indicates if the Y asxis is the UP axis or not. */ |
||||
protected boolean fixUpAxis; |
||||
/** |
||||
* Depending on the blender version rotations are stored in degrees or |
||||
* radians so we need to know the version that is used. |
||||
*/ |
||||
protected final int blenderVersion; |
||||
|
||||
/** |
||||
* Constructor. Stores the bezier curves. |
||||
* |
||||
* @param bezierCurves |
||||
* a table of bezier curves |
||||
* @param fixUpAxis |
||||
* indicates if the Y is the up axis or not |
||||
* @param blenderVersion |
||||
* the blender version that is currently used |
||||
*/ |
||||
public Ipo(BezierCurve[] bezierCurves, boolean fixUpAxis, int blenderVersion) { |
||||
this.bezierCurves = bezierCurves; |
||||
this.fixUpAxis = fixUpAxis; |
||||
this.blenderVersion = blenderVersion; |
||||
} |
||||
|
||||
/** |
||||
* This method calculates the ipo value for the first curve. |
||||
* |
||||
* @param frame |
||||
* the frame for which the value is calculated |
||||
* @return calculated ipo value |
||||
*/ |
||||
public double calculateValue(int frame) { |
||||
return this.calculateValue(frame, 0); |
||||
} |
||||
|
||||
/** |
||||
* This method calculates the ipo value for the curve of the specified |
||||
* index. Make sure you do not exceed the curves amount. Alway chech the |
||||
* amount of curves before calling this method. |
||||
* |
||||
* @param frame |
||||
* the frame for which the value is calculated |
||||
* @param curveIndex |
||||
* the index of the curve |
||||
* @return calculated ipo value |
||||
*/ |
||||
public double calculateValue(int frame, int curveIndex) { |
||||
return bezierCurves[curveIndex].evaluate(frame, BezierCurve.Y_VALUE); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the frame where last bezier triple center point of |
||||
* the specified bezier curve is located. |
||||
* |
||||
* @return the frame number of the last defined bezier triple point for the |
||||
* specified ipo |
||||
*/ |
||||
public int getLastFrame() { |
||||
int result = 1; |
||||
for (int i = 0; i < bezierCurves.length; ++i) { |
||||
int tempResult = bezierCurves[i].getLastFrame(); |
||||
if (tempResult > result) { |
||||
result = tempResult; |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method calculates the value of the curves as a bone track between |
||||
* the specified frames. |
||||
* |
||||
* @param targetIndex |
||||
* the index of the target for which the method calculates the |
||||
* tracks IMPORTANT! Aet to -1 (or any negative number) if you |
||||
* want to load spatial animation. |
||||
* @param localTranslation |
||||
* the local translation of the object/bone that will be animated by |
||||
* the track |
||||
* @param localRotation |
||||
* the local rotation of the object/bone that will be animated by |
||||
* the track |
||||
* @param localScale |
||||
* the local scale of the object/bone that will be animated by |
||||
* the track |
||||
* @param startFrame |
||||
* the first frame of tracks (inclusive) |
||||
* @param stopFrame |
||||
* the last frame of the tracks (inclusive) |
||||
* @param fps |
||||
* frame rate (frames per second) |
||||
* @param spatialTrack |
||||
* this flag indicates if the track belongs to a spatial or to a |
||||
* bone; the difference is important because it appears that bones |
||||
* in blender have the same type of coordinate system (Y as UP) |
||||
* as jme while other features have different one (Z is UP) |
||||
* @return bone track for the specified bone |
||||
*/ |
||||
public Track calculateTrack(int targetIndex, BoneContext boneContext, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean spatialTrack) { |
||||
if (calculatedTrack == null) { |
||||
// preparing data for track
|
||||
int framesAmount = stopFrame - startFrame; |
||||
float timeBetweenFrames = 1.0f / fps; |
||||
|
||||
float[] times = new float[framesAmount + 1]; |
||||
Vector3f[] translations = new Vector3f[framesAmount + 1]; |
||||
float[] translation = new float[3]; |
||||
Quaternion[] rotations = new Quaternion[framesAmount + 1]; |
||||
float[] quaternionRotation = new float[] { localRotation.getX(), localRotation.getY(), localRotation.getZ(), localRotation.getW(), }; |
||||
float[] eulerRotation = localRotation.toAngles(null); |
||||
Vector3f[] scales = new Vector3f[framesAmount + 1]; |
||||
float[] scale = new float[] { localScale.x, localScale.y, localScale.z }; |
||||
float degreeToRadiansFactor = 1; |
||||
if (blenderVersion < 250) {// in blender earlier than 2.50 the values are stored in degrees
|
||||
degreeToRadiansFactor *= FastMath.DEG_TO_RAD * 10;// the values in blender are divided by 10, so we need to mult it here
|
||||
} |
||||
int yIndex = 1, zIndex = 2; |
||||
boolean swapAxes = spatialTrack && fixUpAxis; |
||||
if (swapAxes) { |
||||
yIndex = 2; |
||||
zIndex = 1; |
||||
} |
||||
boolean eulerRotationUsed = false, queternionRotationUsed = false; |
||||
|
||||
// calculating track data
|
||||
for (int frame = startFrame; frame <= stopFrame; ++frame) { |
||||
boolean translationSet = false; |
||||
translation[0] = translation[1] = translation[2] = 0; |
||||
int index = frame - startFrame; |
||||
times[index] = index * timeBetweenFrames;// start + (frame - 1) * timeBetweenFrames;
|
||||
for (int j = 0; j < bezierCurves.length; ++j) { |
||||
double value = bezierCurves[j].evaluate(frame, BezierCurve.Y_VALUE); |
||||
switch (bezierCurves[j].getType()) { |
||||
// LOCATION
|
||||
case AC_LOC_X: |
||||
translation[0] = (float) value; |
||||
translationSet = true; |
||||
break; |
||||
case AC_LOC_Y: |
||||
if (swapAxes && value != 0) { |
||||
value = -value; |
||||
} |
||||
translation[yIndex] = (float) value; |
||||
translationSet = true; |
||||
break; |
||||
case AC_LOC_Z: |
||||
translation[zIndex] = (float) value; |
||||
translationSet = true; |
||||
break; |
||||
|
||||
// EULER ROTATION
|
||||
case OB_ROT_X: |
||||
eulerRotationUsed = true; |
||||
eulerRotation[0] = (float) value * degreeToRadiansFactor; |
||||
break; |
||||
case OB_ROT_Y: |
||||
eulerRotationUsed = true; |
||||
if (swapAxes && value != 0) { |
||||
value = -value; |
||||
} |
||||
eulerRotation[yIndex] = (float) value * degreeToRadiansFactor; |
||||
break; |
||||
case OB_ROT_Z: |
||||
eulerRotationUsed = true; |
||||
eulerRotation[zIndex] = (float) value * degreeToRadiansFactor; |
||||
break; |
||||
|
||||
// SIZE
|
||||
case AC_SIZE_X: |
||||
scale[0] = (float) value; |
||||
break; |
||||
case AC_SIZE_Y: |
||||
scale[yIndex] = (float) value; |
||||
break; |
||||
case AC_SIZE_Z: |
||||
scale[zIndex] = (float) value; |
||||
break; |
||||
|
||||
// QUATERNION ROTATION (used with bone animation)
|
||||
case AC_QUAT_W: |
||||
queternionRotationUsed = true; |
||||
quaternionRotation[3] = (float) value; |
||||
break; |
||||
case AC_QUAT_X: |
||||
queternionRotationUsed = true; |
||||
quaternionRotation[0] = (float) value; |
||||
break; |
||||
case AC_QUAT_Y: |
||||
queternionRotationUsed = true; |
||||
if (swapAxes && value != 0) { |
||||
value = -value; |
||||
} |
||||
quaternionRotation[yIndex] = (float) value; |
||||
break; |
||||
case AC_QUAT_Z: |
||||
quaternionRotation[zIndex] = (float) value; |
||||
break; |
||||
default: |
||||
LOGGER.log(Level.WARNING, "Unknown ipo curve type: {0}.", bezierCurves[j].getType()); |
||||
} |
||||
} |
||||
if(translationSet) { |
||||
translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2])); |
||||
} else { |
||||
translations[index] = new Vector3f(); |
||||
} |
||||
|
||||
if(boneContext != null) { |
||||
if(boneContext.getBone().getParent() == null && boneContext.is(BoneContext.NO_LOCAL_LOCATION)) { |
||||
float temp = translations[index].z; |
||||
translations[index].z = -translations[index].y; |
||||
translations[index].y = temp; |
||||
} |
||||
} |
||||
|
||||
if (queternionRotationUsed) { |
||||
rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]); |
||||
} else { |
||||
rotations[index] = new Quaternion().fromAngles(eulerRotation); |
||||
} |
||||
|
||||
scales[index] = new Vector3f(scale[0], scale[1], scale[2]); |
||||
} |
||||
if (spatialTrack) { |
||||
calculatedTrack = new SpatialTrack(times, translations, rotations, scales); |
||||
} else { |
||||
calculatedTrack = new BoneTrack(targetIndex, times, translations, rotations, scales); |
||||
} |
||||
|
||||
if (queternionRotationUsed && eulerRotationUsed) { |
||||
LOGGER.warning("Animation uses both euler and quaternion tracks for rotations. Quaternion rotation is applied. Make sure that this is what you wanted!"); |
||||
} |
||||
} |
||||
|
||||
return calculatedTrack; |
||||
} |
||||
|
||||
/** |
||||
* Ipo constant curve. This is a curve with only one value and no specified |
||||
* type. This type of ipo cannot be used to calculate tracks. It should only |
||||
* be used to calculate single value for a given frame. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */static class ConstIpo extends Ipo { |
||||
|
||||
/** The constant value of this ipo. */ |
||||
private float constValue; |
||||
|
||||
/** |
||||
* Constructor. Stores the constant value of this ipo. |
||||
* |
||||
* @param constValue |
||||
* the constant value of this ipo |
||||
*/ |
||||
public ConstIpo(float constValue) { |
||||
super(null, false, 0);// the version is not important here
|
||||
this.constValue = constValue; |
||||
} |
||||
|
||||
@Override |
||||
public double calculateValue(int frame) { |
||||
return constValue; |
||||
} |
||||
|
||||
@Override |
||||
public double calculateValue(int frame, int curveIndex) { |
||||
return constValue; |
||||
} |
||||
|
||||
@Override |
||||
public BoneTrack calculateTrack(int boneIndex, BoneContext boneContext, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean boneTrack) { |
||||
throw new IllegalStateException("Constatnt ipo object cannot be used for calculating bone tracks!"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,148 @@ |
||||
package com.jme3.scene.plugins.blender.cameras; |
||||
|
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.renderer.Camera; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A class that is used to load cameras into the scene. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class CameraHelper extends AbstractBlenderHelper { |
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(CameraHelper.class.getName()); |
||||
protected static final int DEFAULT_CAM_WIDTH = 640; |
||||
protected static final int DEFAULT_CAM_HEIGHT = 480; |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in |
||||
* different blender versions. |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public CameraHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given structure to jme camera. |
||||
* |
||||
* @param structure |
||||
* camera structure |
||||
* @return jme camera object |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there are problems with the |
||||
* blender file |
||||
*/ |
||||
public Camera toCamera(Structure structure, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (blenderVersion >= 250) { |
||||
return this.toCamera250(structure, blenderContext.getSceneStructure()); |
||||
} else { |
||||
return this.toCamera249(structure); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given structure to jme camera. Should be used form blender 2.5+. |
||||
* |
||||
* @param structure |
||||
* camera structure |
||||
* @param sceneStructure |
||||
* scene structure |
||||
* @return jme camera object |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there are problems with the |
||||
* blender file |
||||
*/ |
||||
private Camera toCamera250(Structure structure, Structure sceneStructure) throws BlenderFileException { |
||||
int width = DEFAULT_CAM_WIDTH; |
||||
int height = DEFAULT_CAM_HEIGHT; |
||||
if (sceneStructure != null) { |
||||
Structure renderData = (Structure) sceneStructure.getFieldValue("r"); |
||||
width = ((Number) renderData.getFieldValue("xsch")).shortValue(); |
||||
height = ((Number) renderData.getFieldValue("ysch")).shortValue(); |
||||
} |
||||
Camera camera = new Camera(width, height); |
||||
int type = ((Number) structure.getFieldValue("type")).intValue(); |
||||
if (type != 0 && type != 1) { |
||||
LOGGER.log(Level.WARNING, "Unknown camera type: {0}. Perspective camera is being used!", type); |
||||
type = 0; |
||||
} |
||||
// type==0 - perspective; type==1 - orthographic; perspective is used as default
|
||||
camera.setParallelProjection(type == 1); |
||||
float aspect = width / (float) height; |
||||
float fovY; // Vertical field of view in degrees
|
||||
float clipsta = ((Number) structure.getFieldValue("clipsta")).floatValue(); |
||||
float clipend = ((Number) structure.getFieldValue("clipend")).floatValue(); |
||||
if (type == 0) { |
||||
// Convert lens MM to vertical degrees in fovY, see Blender rna_Camera_angle_get()
|
||||
// Default sensor size prior to 2.60 was 32.
|
||||
float sensor = 32.0f; |
||||
boolean sensorVertical = false; |
||||
Number sensorFit = (Number) structure.getFieldValue("sensor_fit"); |
||||
if (sensorFit != null) { |
||||
// If sensor_fit is vert (2), then sensor_y is used
|
||||
sensorVertical = sensorFit.byteValue() == 2; |
||||
String sensorName = "sensor_x"; |
||||
if (sensorVertical) { |
||||
sensorName = "sensor_y"; |
||||
} |
||||
sensor = ((Number) structure.getFieldValue(sensorName)).floatValue(); |
||||
} |
||||
float focalLength = ((Number) structure.getFieldValue("lens")).floatValue(); |
||||
float fov = 2.0f * FastMath.atan(sensor / 2.0f / focalLength); |
||||
if (sensorVertical) { |
||||
fovY = fov * FastMath.RAD_TO_DEG; |
||||
} else { |
||||
// Convert fov from horizontal to vertical
|
||||
fovY = 2.0f * FastMath.atan(FastMath.tan(fov / 2.0f) / aspect) * FastMath.RAD_TO_DEG; |
||||
} |
||||
} else { |
||||
// This probably is not correct.
|
||||
fovY = ((Number) structure.getFieldValue("ortho_scale")).floatValue(); |
||||
} |
||||
camera.setFrustumPerspective(fovY, aspect, clipsta, clipend); |
||||
camera.setName(structure.getName()); |
||||
return camera; |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given structure to jme camera. Should be used form blender 2.49. |
||||
* |
||||
* @param structure |
||||
* camera structure |
||||
* @return jme camera object |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there are problems with the |
||||
* blender file |
||||
*/ |
||||
private Camera toCamera249(Structure structure) throws BlenderFileException { |
||||
Camera camera = new Camera(DEFAULT_CAM_WIDTH, DEFAULT_CAM_HEIGHT); |
||||
int type = ((Number) structure.getFieldValue("type")).intValue(); |
||||
if (type != 0 && type != 1) { |
||||
LOGGER.log(Level.WARNING, "Unknown camera type: {0}. Perspective camera is being used!", type); |
||||
type = 0; |
||||
} |
||||
// type==0 - perspective; type==1 - orthographic; perspective is used as default
|
||||
camera.setParallelProjection(type == 1); |
||||
float aspect = 0; |
||||
float clipsta = ((Number) structure.getFieldValue("clipsta")).floatValue(); |
||||
float clipend = ((Number) structure.getFieldValue("clipend")).floatValue(); |
||||
if (type == 0) { |
||||
aspect = ((Number) structure.getFieldValue("lens")).floatValue(); |
||||
} else { |
||||
aspect = ((Number) structure.getFieldValue("ortho_scale")).floatValue(); |
||||
} |
||||
camera.setFrustumPerspective(aspect, camera.getWidth() / camera.getHeight(), clipsta, clipend); |
||||
camera.setName(structure.getName()); |
||||
return camera; |
||||
} |
||||
} |
@ -0,0 +1,88 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.animations.Ipo; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
|
||||
/** |
||||
* Constraint applied on the bone. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class BoneConstraint extends Constraint { |
||||
private static final Logger LOGGER = Logger.getLogger(BoneConstraint.class.getName()); |
||||
|
||||
/** |
||||
* The bone constraint constructor. |
||||
* |
||||
* @param constraintStructure |
||||
* the constraint's structure |
||||
* @param ownerOMA |
||||
* the OMA of the bone that owns the constraint |
||||
* @param influenceIpo |
||||
* the influence interpolation curve |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* exception thrown when problems with blender file occur |
||||
*/ |
||||
public BoneConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException { |
||||
super(constraintStructure, ownerOMA, influenceIpo, blenderContext); |
||||
} |
||||
|
||||
@Override |
||||
public boolean validate() { |
||||
if (targetOMA != null) { |
||||
Spatial nodeTarget = (Spatial) blenderContext.getLoadedFeature(targetOMA, LoadedDataType.FEATURE); |
||||
if (nodeTarget == null) { |
||||
LOGGER.log(Level.WARNING, "Cannot find target for constraint: {0}.", name); |
||||
return false; |
||||
} |
||||
// the second part of the if expression verifies if the found node
|
||||
// (if any) is an armature node
|
||||
if (blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, nodeTarget) != null) { |
||||
if (subtargetName.trim().isEmpty()) { |
||||
LOGGER.log(Level.WARNING, "No bone target specified for constraint: {0}.", name); |
||||
return false; |
||||
} |
||||
// if the target is not an object node then it is an Armature,
|
||||
// so make sure the bone is in the current skeleton
|
||||
BoneContext boneContext = blenderContext.getBoneContext(ownerOMA); |
||||
if (targetOMA.longValue() != boneContext.getArmatureObjectOMA().longValue()) { |
||||
LOGGER.log(Level.WARNING, "Bone constraint {0} must target bone in the its own skeleton! Targeting bone in another skeleton is not supported!", name); |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
return constraintDefinition == null ? true : constraintDefinition.isTargetRequired(); |
||||
} |
||||
|
||||
@Override |
||||
public void apply(int frame) { |
||||
super.apply(frame); |
||||
blenderContext.getBoneContext(ownerOMA).getBone().updateModelTransforms(); |
||||
} |
||||
|
||||
@Override |
||||
public Long getTargetOMA() { |
||||
if(targetOMA != null && subtargetName != null && !subtargetName.trim().isEmpty()) { |
||||
Spatial nodeTarget = (Spatial) blenderContext.getLoadedFeature(targetOMA, LoadedDataType.FEATURE); |
||||
if(nodeTarget != null) { |
||||
if(blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, nodeTarget) != null) { |
||||
BoneContext boneContext = blenderContext.getBoneByName(targetOMA, subtargetName); |
||||
return boneContext != null ? boneContext.getBoneOma() : 0L; |
||||
} |
||||
return targetOMA; |
||||
} |
||||
} |
||||
return 0L; |
||||
} |
||||
} |
@ -0,0 +1,186 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.Ipo; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinition; |
||||
import com.jme3.scene.plugins.blender.constraints.definitions.ConstraintDefinitionFactory; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* The implementation of a constraint. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public abstract class Constraint { |
||||
private static final Logger LOGGER = Logger.getLogger(Constraint.class.getName()); |
||||
|
||||
/** The name of this constraint. */ |
||||
protected final String name; |
||||
/** Indicates if the constraint is already baked or not. */ |
||||
protected boolean baked; |
||||
|
||||
protected Space ownerSpace; |
||||
protected final ConstraintDefinition constraintDefinition; |
||||
protected Long ownerOMA; |
||||
|
||||
protected Long targetOMA; |
||||
protected Space targetSpace; |
||||
protected String subtargetName; |
||||
|
||||
/** The ipo object defining influence. */ |
||||
protected final Ipo ipo; |
||||
/** The blender context. */ |
||||
protected final BlenderContext blenderContext; |
||||
protected final ConstraintHelper constraintHelper; |
||||
|
||||
/** |
||||
* This constructor creates the constraint instance. |
||||
* |
||||
* @param constraintStructure |
||||
* the constraint's structure (bConstraint clss in blender 2.49). |
||||
* @param ownerOMA |
||||
* the old memory address of the constraint owner |
||||
* @param influenceIpo |
||||
* the ipo curve of the influence factor |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public Constraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException { |
||||
this.blenderContext = blenderContext; |
||||
name = constraintStructure.getFieldValue("name").toString(); |
||||
Pointer pData = (Pointer) constraintStructure.getFieldValue("data"); |
||||
if (pData.isNotNull()) { |
||||
Structure data = pData.fetchData().get(0); |
||||
constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(data, name, ownerOMA, blenderContext); |
||||
Pointer pTar = (Pointer) data.getFieldValue("tar"); |
||||
if (pTar != null && pTar.isNotNull()) { |
||||
targetOMA = pTar.getOldMemoryAddress(); |
||||
targetSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("tarspace")).byteValue()); |
||||
Object subtargetValue = data.getFieldValue("subtarget"); |
||||
if (subtargetValue != null) {// not all constraint data have the
|
||||
// subtarget field
|
||||
subtargetName = subtargetValue.toString(); |
||||
} |
||||
} |
||||
} else { |
||||
// Null constraint has no data, so create it here
|
||||
constraintDefinition = ConstraintDefinitionFactory.createConstraintDefinition(null, name, null, blenderContext); |
||||
} |
||||
ownerSpace = Space.valueOf(((Number) constraintStructure.getFieldValue("ownspace")).byteValue()); |
||||
ipo = influenceIpo; |
||||
this.ownerOMA = ownerOMA; |
||||
constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
LOGGER.log(Level.INFO, "Created constraint: {0} with definition: {1}", new Object[] { name, constraintDefinition }); |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the constraint is implemented and <b>false</b> |
||||
* otherwise |
||||
*/ |
||||
public boolean isImplemented() { |
||||
return constraintDefinition == null ? true : constraintDefinition.isImplemented(); |
||||
} |
||||
|
||||
/** |
||||
* @return the name of the constraint type, similar to the constraint name |
||||
* used in Blender |
||||
*/ |
||||
public String getConstraintTypeName() { |
||||
return constraintDefinition.getConstraintTypeName(); |
||||
} |
||||
|
||||
/** |
||||
* @return the OMAs of the features whose transform had been altered beside the constraint owner |
||||
*/ |
||||
public Set<Long> getAlteredOmas() { |
||||
return constraintDefinition.getAlteredOmas(); |
||||
} |
||||
|
||||
/** |
||||
* Performs validation before baking. Checks factors that can prevent |
||||
* constraint from baking that could not be checked during constraint |
||||
* loading. |
||||
*/ |
||||
public abstract boolean validate(); |
||||
|
||||
/** |
||||
* @return the OMA of the target or 0 if no target is specified for the constraint |
||||
*/ |
||||
public abstract Long getTargetOMA(); |
||||
|
||||
/** |
||||
* Applies the constraint to owner (and in some cases can alter other bones of the skeleton). |
||||
* @param frame |
||||
* the frame of the animation |
||||
*/ |
||||
public void apply(int frame) { |
||||
if (LOGGER.isLoggable(Level.FINEST)) { |
||||
LOGGER.log(Level.FINEST, "Applying constraint: {0} for frame {1}", new Object[] { name, frame }); |
||||
} |
||||
Transform targetTransform = targetOMA != null ? constraintHelper.getTransform(targetOMA, subtargetName, targetSpace) : null; |
||||
constraintDefinition.bake(ownerSpace, targetSpace, targetTransform, (float) ipo.calculateValue(frame)); |
||||
} |
||||
|
||||
/** |
||||
* @return determines if the definition of the constraint will change the bone in any way; in most cases |
||||
* it is possible to tell that even before the constraint baking simulation is started, so we can discard such bones from constraint |
||||
* computing to improve the computation speed and lower the computations complexity |
||||
*/ |
||||
public boolean isTrackToBeChanged() { |
||||
return constraintDefinition == null ? false : constraintDefinition.isTrackToBeChanged(); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
final int prime = 31; |
||||
int result = 1; |
||||
result = prime * result + (name == null ? 0 : name.hashCode()); |
||||
result = prime * result + (ownerOMA == null ? 0 : ownerOMA.hashCode()); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (obj == null) { |
||||
return false; |
||||
} |
||||
if (this.getClass() != obj.getClass()) { |
||||
return false; |
||||
} |
||||
Constraint other = (Constraint) obj; |
||||
if (name == null) { |
||||
if (other.name != null) { |
||||
return false; |
||||
} |
||||
} else if (!name.equals(other.name)) { |
||||
return false; |
||||
} |
||||
if (ownerOMA == null) { |
||||
if (other.ownerOMA != null) { |
||||
return false; |
||||
} |
||||
} else if (!ownerOMA.equals(other.ownerOMA)) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "Constraint(name = " + name + ", def = " + constraintDefinition + ")"; |
||||
} |
||||
} |
@ -0,0 +1,476 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.AnimationHelper; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.animations.Ipo; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
import com.jme3.util.TempVars; |
||||
|
||||
/** |
||||
* This class should be used for constraint calculations. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class ConstraintHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(ConstraintHelper.class.getName()); |
||||
|
||||
/** |
||||
* Helper constructor. |
||||
* |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public ConstraintHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* This method reads constraints for for the given structure. The |
||||
* constraints are loaded only once for object/bone. |
||||
* |
||||
* @param objectStructure |
||||
* the structure we read constraint's for |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
*/ |
||||
public void loadConstraints(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.fine("Loading constraints."); |
||||
// reading influence ipos for the constraints
|
||||
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); |
||||
Map<String, Map<String, Ipo>> constraintsIpos = new HashMap<String, Map<String, Ipo>>(); |
||||
Pointer pActions = (Pointer) objectStructure.getFieldValue("action"); |
||||
if (pActions.isNotNull()) { |
||||
List<Structure> actions = pActions.fetchData(); |
||||
for (Structure action : actions) { |
||||
Structure chanbase = (Structure) action.getFieldValue("chanbase"); |
||||
List<Structure> actionChannels = chanbase.evaluateListBase(); |
||||
for (Structure actionChannel : actionChannels) { |
||||
Map<String, Ipo> ipos = new HashMap<String, Ipo>(); |
||||
Structure constChannels = (Structure) actionChannel.getFieldValue("constraintChannels"); |
||||
List<Structure> constraintChannels = constChannels.evaluateListBase(); |
||||
for (Structure constraintChannel : constraintChannels) { |
||||
Pointer pIpo = (Pointer) constraintChannel.getFieldValue("ipo"); |
||||
if (pIpo.isNotNull()) { |
||||
String constraintName = constraintChannel.getFieldValue("name").toString(); |
||||
Ipo ipo = animationHelper.fromIpoStructure(pIpo.fetchData().get(0), blenderContext); |
||||
ipos.put(constraintName, ipo); |
||||
} |
||||
} |
||||
String actionName = actionChannel.getFieldValue("name").toString(); |
||||
constraintsIpos.put(actionName, ipos); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// loading constraints connected with the object's bones
|
||||
Pointer pPose = (Pointer) objectStructure.getFieldValue("pose"); |
||||
if (pPose.isNotNull()) { |
||||
List<Structure> poseChannels = ((Structure) pPose.fetchData().get(0).getFieldValue("chanbase")).evaluateListBase(); |
||||
for (Structure poseChannel : poseChannels) { |
||||
List<Constraint> constraintsList = new ArrayList<Constraint>(); |
||||
Long boneOMA = Long.valueOf(((Pointer) poseChannel.getFieldValue("bone")).getOldMemoryAddress()); |
||||
|
||||
// the name is read directly from structure because bone might
|
||||
// not yet be loaded
|
||||
String name = blenderContext.getFileBlock(boneOMA).getStructure(blenderContext).getFieldValue("name").toString(); |
||||
List<Structure> constraints = ((Structure) poseChannel.getFieldValue("constraints")).evaluateListBase(); |
||||
for (Structure constraint : constraints) { |
||||
String constraintName = constraint.getFieldValue("name").toString(); |
||||
Map<String, Ipo> ipoMap = constraintsIpos.get(name); |
||||
Ipo ipo = ipoMap == null ? null : ipoMap.get(constraintName); |
||||
if (ipo == null) { |
||||
float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue(); |
||||
ipo = animationHelper.fromValue(enforce); |
||||
} |
||||
constraintsList.add(new BoneConstraint(constraint, boneOMA, ipo, blenderContext)); |
||||
} |
||||
blenderContext.addConstraints(boneOMA, constraintsList); |
||||
} |
||||
} |
||||
|
||||
// loading constraints connected with the object itself
|
||||
List<Structure> constraints = ((Structure) objectStructure.getFieldValue("constraints")).evaluateListBase(); |
||||
if (constraints != null && constraints.size() > 0) { |
||||
Pointer pData = (Pointer) objectStructure.getFieldValue("data"); |
||||
String dataType = pData.isNotNull() ? pData.fetchData().get(0).getType() : null; |
||||
List<Constraint> constraintsList = new ArrayList<Constraint>(constraints.size()); |
||||
|
||||
for (Structure constraint : constraints) { |
||||
String constraintName = constraint.getFieldValue("name").toString(); |
||||
String objectName = objectStructure.getName(); |
||||
|
||||
Map<String, Ipo> objectConstraintsIpos = constraintsIpos.get(objectName); |
||||
Ipo ipo = objectConstraintsIpos != null ? objectConstraintsIpos.get(constraintName) : null; |
||||
if (ipo == null) { |
||||
float enforce = ((Number) constraint.getFieldValue("enforce")).floatValue(); |
||||
ipo = animationHelper.fromValue(enforce); |
||||
} |
||||
|
||||
constraintsList.add(this.createConstraint(dataType, constraint, objectStructure.getOldMemoryAddress(), ipo, blenderContext)); |
||||
} |
||||
blenderContext.addConstraints(objectStructure.getOldMemoryAddress(), constraintsList); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method creates a proper constraint object depending on the object's |
||||
* data type. Supported data types: <li>Mesh <li>Armature <li>Camera <li> |
||||
* Lamp Bone constraints are created in a different place. |
||||
* |
||||
* @param dataType |
||||
* the type of the object's data |
||||
* @param constraintStructure |
||||
* the constraint structure |
||||
* @param ownerOMA |
||||
* the owner OMA |
||||
* @param influenceIpo |
||||
* the influence interpolation curve |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return constraint object for the required type |
||||
* @throws BlenderFileException |
||||
* thrown when problems with blender file occurred |
||||
*/ |
||||
private Constraint createConstraint(String dataType, Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (dataType == null || "Mesh".equalsIgnoreCase(dataType) || "Camera".equalsIgnoreCase(dataType) || "Lamp".equalsIgnoreCase(dataType)) { |
||||
return new SpatialConstraint(constraintStructure, ownerOMA, influenceIpo, blenderContext); |
||||
} else if ("Armature".equalsIgnoreCase(dataType)) { |
||||
return new SkeletonConstraint(constraintStructure, ownerOMA, influenceIpo, blenderContext); |
||||
} else { |
||||
throw new IllegalArgumentException("Unsupported data type for applying constraints: " + dataType); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method bakes all available and valid constraints. |
||||
* |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public void bakeConstraints(BlenderContext blenderContext) { |
||||
Set<Long> owners = new HashSet<Long>(); |
||||
for (Constraint constraint : blenderContext.getAllConstraints()) { |
||||
if(constraint instanceof BoneConstraint) { |
||||
BoneContext boneContext = blenderContext.getBoneContext(constraint.ownerOMA); |
||||
owners.add(boneContext.getArmatureObjectOMA()); |
||||
} else { |
||||
Spatial spatial = (Spatial) blenderContext.getLoadedFeature(constraint.ownerOMA, LoadedDataType.FEATURE); |
||||
while (spatial.getParent() != null) { |
||||
spatial = spatial.getParent(); |
||||
} |
||||
owners.add((Long)blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, spatial)); |
||||
} |
||||
} |
||||
|
||||
List<SimulationNode> simulationRootNodes = new ArrayList<SimulationNode>(owners.size()); |
||||
for(Long ownerOMA : owners) { |
||||
simulationRootNodes.add(new SimulationNode(ownerOMA, blenderContext)); |
||||
} |
||||
|
||||
for (SimulationNode node : simulationRootNodes) { |
||||
node.simulate(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method retrieves the transform from a feature in a given space. |
||||
* |
||||
* @param oma |
||||
* the OMA of the feature (spatial or armature node) |
||||
* @param subtargetName |
||||
* the feature's subtarget (bone in a case of armature's node) |
||||
* @param space |
||||
* the space the transform is evaluated to |
||||
* @return the transform of a feature in a given space |
||||
*/ |
||||
public Transform getTransform(Long oma, String subtargetName, Space space) { |
||||
Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedDataType.FEATURE); |
||||
boolean isArmature = blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, feature) != null; |
||||
if (isArmature) { |
||||
blenderContext.getSkeleton(oma).updateWorldVectors(); |
||||
BoneContext targetBoneContext = blenderContext.getBoneByName(oma, subtargetName); |
||||
Bone bone = targetBoneContext.getBone(); |
||||
|
||||
if (bone.getParent() == null && (space == Space.CONSTRAINT_SPACE_LOCAL || space == Space.CONSTRAINT_SPACE_PARLOCAL)) { |
||||
space = Space.CONSTRAINT_SPACE_POSE; |
||||
} |
||||
|
||||
TempVars tempVars = TempVars.get();// use readable names of the matrices so that the code is more clear
|
||||
Transform result; |
||||
switch (space) { |
||||
case CONSTRAINT_SPACE_WORLD: |
||||
Spatial model = (Spatial) blenderContext.getLoadedFeature(targetBoneContext.getSkeletonOwnerOma(), LoadedDataType.FEATURE); |
||||
Matrix4f boneModelMatrix = this.toMatrix(bone.getModelSpacePosition(), bone.getModelSpaceRotation(), bone.getModelSpaceScale(), tempVars.tempMat4); |
||||
Matrix4f modelWorldMatrix = this.toMatrix(model.getWorldTransform(), tempVars.tempMat42); |
||||
Matrix4f boneMatrixInWorldSpace = modelWorldMatrix.multLocal(boneModelMatrix); |
||||
result = new Transform(boneMatrixInWorldSpace.toTranslationVector(), boneMatrixInWorldSpace.toRotationQuat(), boneMatrixInWorldSpace.toScaleVector()); |
||||
break; |
||||
case CONSTRAINT_SPACE_LOCAL: |
||||
assert bone.getParent() != null : "CONSTRAINT_SPACE_LOCAL should be evaluated as CONSTRAINT_SPACE_POSE if the bone has no parent!"; |
||||
result = new Transform(bone.getLocalPosition(), bone.getLocalRotation(), bone.getLocalScale()); |
||||
break; |
||||
case CONSTRAINT_SPACE_POSE: { |
||||
Matrix4f boneWorldMatrix = this.toMatrix(this.getTransform(oma, subtargetName, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat4); |
||||
Matrix4f armatureInvertedWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat42).invertLocal(); |
||||
Matrix4f bonePoseMatrix = armatureInvertedWorldMatrix.multLocal(boneWorldMatrix); |
||||
result = new Transform(bonePoseMatrix.toTranslationVector(), bonePoseMatrix.toRotationQuat(), bonePoseMatrix.toScaleVector()); |
||||
break; |
||||
} |
||||
case CONSTRAINT_SPACE_PARLOCAL: { |
||||
Matrix4f boneWorldMatrix = this.toMatrix(this.getTransform(oma, subtargetName, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat4); |
||||
Matrix4f armatureInvertedWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat42).invertLocal(); |
||||
Matrix4f bonePoseMatrix = armatureInvertedWorldMatrix.multLocal(boneWorldMatrix); |
||||
result = new Transform(bonePoseMatrix.toTranslationVector(), bonePoseMatrix.toRotationQuat(), bonePoseMatrix.toScaleVector()); |
||||
Bone parent = bone.getParent(); |
||||
if(parent != null) { |
||||
BoneContext parentContext = blenderContext.getBoneContext(parent); |
||||
Vector3f head = parent.getModelSpacePosition(); |
||||
Vector3f tail = head.add(bone.getModelSpaceRotation().mult(Vector3f.UNIT_Y.mult(parentContext.getLength()))); |
||||
result.getTranslation().subtractLocal(tail); |
||||
|
||||
} |
||||
break; |
||||
} |
||||
default: |
||||
throw new IllegalStateException("Unknown space type: " + space); |
||||
} |
||||
tempVars.release(); |
||||
return result; |
||||
} else { |
||||
switch (space) { |
||||
case CONSTRAINT_SPACE_LOCAL: |
||||
return feature.getLocalTransform(); |
||||
case CONSTRAINT_SPACE_WORLD: |
||||
return feature.getWorldTransform(); |
||||
case CONSTRAINT_SPACE_PARLOCAL: |
||||
case CONSTRAINT_SPACE_POSE: |
||||
throw new IllegalStateException("Nodes can have only Local and World spaces applied!"); |
||||
default: |
||||
throw new IllegalStateException("Unknown space type: " + space); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Applies transform to a feature (bone or spatial). Computations transform |
||||
* the given transformation from the given space to the feature's local |
||||
* space. |
||||
* |
||||
* @param oma |
||||
* the OMA of the feature we apply transformation to |
||||
* @param subtargetName |
||||
* the name of the feature's subtarget (bone in case of armature) |
||||
* @param space |
||||
* the space in which the given transform is to be applied |
||||
* @param transform |
||||
* the transform we apply |
||||
*/ |
||||
public void applyTransform(Long oma, String subtargetName, Space space, Transform transform) { |
||||
Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedDataType.FEATURE); |
||||
boolean isArmature = blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, feature) != null; |
||||
if (isArmature) { |
||||
Skeleton skeleton = blenderContext.getSkeleton(oma); |
||||
BoneContext targetBoneContext = blenderContext.getBoneByName(oma, subtargetName); |
||||
Bone bone = targetBoneContext.getBone(); |
||||
|
||||
if (bone.getParent() == null && (space == Space.CONSTRAINT_SPACE_LOCAL || space == Space.CONSTRAINT_SPACE_PARLOCAL)) { |
||||
space = Space.CONSTRAINT_SPACE_POSE; |
||||
} |
||||
|
||||
TempVars tempVars = TempVars.get(); |
||||
switch (space) { |
||||
case CONSTRAINT_SPACE_LOCAL: |
||||
assert bone.getParent() != null : "CONSTRAINT_SPACE_LOCAL should be evaluated as CONSTRAINT_SPACE_POSE if the bone has no parent!"; |
||||
bone.setBindTransforms(transform.getTranslation(), transform.getRotation(), transform.getScale()); |
||||
break; |
||||
case CONSTRAINT_SPACE_WORLD: { |
||||
Matrix4f boneMatrixInWorldSpace = this.toMatrix(transform, tempVars.tempMat4); |
||||
Matrix4f modelWorldMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42); |
||||
Matrix4f boneMatrixInModelSpace = modelWorldMatrix.invertLocal().multLocal(boneMatrixInWorldSpace); |
||||
Bone parent = bone.getParent(); |
||||
if (parent != null) { |
||||
Matrix4f parentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4); |
||||
boneMatrixInModelSpace = parentMatrixInModelSpace.invertLocal().multLocal(boneMatrixInModelSpace); |
||||
} |
||||
bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector()); |
||||
break; |
||||
} |
||||
case CONSTRAINT_SPACE_POSE: { |
||||
Matrix4f armatureWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat4); |
||||
Matrix4f boneMatrixInWorldSpace = armatureWorldMatrix.multLocal(this.toMatrix(transform, tempVars.tempMat42)); |
||||
Matrix4f invertedModelMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42).invertLocal(); |
||||
Matrix4f boneMatrixInModelSpace = invertedModelMatrix.multLocal(boneMatrixInWorldSpace); |
||||
Bone parent = bone.getParent(); |
||||
if (parent != null) { |
||||
Matrix4f parentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4); |
||||
boneMatrixInModelSpace = parentMatrixInModelSpace.invertLocal().multLocal(boneMatrixInModelSpace); |
||||
} |
||||
bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector()); |
||||
break; |
||||
} |
||||
case CONSTRAINT_SPACE_PARLOCAL: |
||||
Matrix4f armatureWorldMatrix = this.toMatrix(feature.getWorldTransform(), tempVars.tempMat4); |
||||
Matrix4f boneMatrixInWorldSpace = armatureWorldMatrix.multLocal(this.toMatrix(transform, tempVars.tempMat42)); |
||||
Matrix4f invertedModelMatrix = this.toMatrix(this.getTransform(targetBoneContext.getSkeletonOwnerOma(), null, Space.CONSTRAINT_SPACE_WORLD), tempVars.tempMat42).invertLocal(); |
||||
Matrix4f boneMatrixInModelSpace = invertedModelMatrix.multLocal(boneMatrixInWorldSpace); |
||||
Bone parent = bone.getParent(); |
||||
if (parent != null) { |
||||
//first add the initial parent matrix to the bone's model matrix
|
||||
BoneContext parentContext = blenderContext.getBoneContext(parent); |
||||
|
||||
Matrix4f initialParentMatrixInModelSpace = parentContext.getBoneMatrixInModelSpace(); |
||||
Matrix4f currentParentMatrixInModelSpace = this.toMatrix(parent.getModelSpacePosition(), parent.getModelSpaceRotation(), parent.getModelSpaceScale(), tempVars.tempMat4); |
||||
//the bone will now move with its parent in model space
|
||||
|
||||
//now we need to subtract the difference between current parent's model matrix and its initial model matrix
|
||||
boneMatrixInModelSpace = initialParentMatrixInModelSpace.mult(boneMatrixInModelSpace); |
||||
|
||||
Matrix4f diffMatrix = initialParentMatrixInModelSpace.mult(currentParentMatrixInModelSpace.invert()); |
||||
boneMatrixInModelSpace.multLocal(diffMatrix); |
||||
//now the bone will have its position in model space with initial parent's model matrix added
|
||||
} |
||||
bone.setBindTransforms(boneMatrixInModelSpace.toTranslationVector(), boneMatrixInModelSpace.toRotationQuat(), boneMatrixInModelSpace.toScaleVector()); |
||||
break; |
||||
default: |
||||
tempVars.release(); |
||||
throw new IllegalStateException("Invalid space type for target object: " + space.toString()); |
||||
} |
||||
tempVars.release(); |
||||
skeleton.updateWorldVectors(); |
||||
} else { |
||||
switch (space) { |
||||
case CONSTRAINT_SPACE_LOCAL: |
||||
feature.getLocalTransform().set(transform); |
||||
break; |
||||
case CONSTRAINT_SPACE_WORLD: |
||||
if (feature.getParent() == null) { |
||||
feature.setLocalTransform(transform); |
||||
} else { |
||||
Transform parentWorldTransform = feature.getParent().getWorldTransform(); |
||||
|
||||
TempVars tempVars = TempVars.get(); |
||||
Matrix4f parentInverseMatrix = this.toMatrix(parentWorldTransform, tempVars.tempMat4).invertLocal(); |
||||
Matrix4f m = this.toMatrix(transform, tempVars.tempMat42); |
||||
m = m.multLocal(parentInverseMatrix); |
||||
tempVars.release(); |
||||
|
||||
transform.setTranslation(m.toTranslationVector()); |
||||
transform.setRotation(m.toRotationQuat()); |
||||
transform.setScale(m.toScaleVector()); |
||||
|
||||
feature.setLocalTransform(transform); |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Invalid space type for spatial object: " + space.toString()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Converts given transform to the matrix. |
||||
* |
||||
* @param transform |
||||
* the transform to be converted |
||||
* @param store |
||||
* the matrix where the result will be stored |
||||
* @return the store matrix |
||||
*/ |
||||
public Matrix4f toMatrix(Transform transform, Matrix4f store) { |
||||
if (transform != null) { |
||||
return this.toMatrix(transform.getTranslation(), transform.getRotation(), transform.getScale(), store); |
||||
} |
||||
store.loadIdentity(); |
||||
return store; |
||||
} |
||||
|
||||
/** |
||||
* Converts given transformation parameters into the matrix. |
||||
* |
||||
* @param position |
||||
* the position of the feature |
||||
* @param rotation |
||||
* the rotation of the feature |
||||
* @param scale |
||||
* the scale of the feature |
||||
* @param store |
||||
* the matrix where the result will be stored |
||||
* @return the store matrix |
||||
*/ |
||||
private Matrix4f toMatrix(Vector3f position, Quaternion rotation, Vector3f scale, Matrix4f store) { |
||||
store.loadIdentity(); |
||||
store.setTranslation(position); |
||||
store.setRotationQuaternion(rotation); |
||||
store.setScale(scale); |
||||
return store; |
||||
} |
||||
|
||||
/** |
||||
* The space of target or owner transformation. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static enum Space { |
||||
/** A transformation of the bone or spatial in the world space. */ |
||||
CONSTRAINT_SPACE_WORLD, |
||||
/** |
||||
* For spatial it is the transformation in its parent space or in WORLD space if it has no parent. |
||||
* For bone it is a transformation in its bone parent space or in armature space if it has no parent. |
||||
*/ |
||||
CONSTRAINT_SPACE_LOCAL, |
||||
/** |
||||
* This space IS NOT applicable for spatials. |
||||
* For bone it is a transformation in the blender's armature object space. |
||||
*/ |
||||
CONSTRAINT_SPACE_POSE, |
||||
|
||||
CONSTRAINT_SPACE_PARLOCAL; |
||||
|
||||
/** |
||||
* This method returns the enum instance when given the appropriate |
||||
* value from the blend file. |
||||
* |
||||
* @param c |
||||
* the blender's value of the space modifier |
||||
* @return the scape enum instance |
||||
*/ |
||||
public static Space valueOf(byte c) { |
||||
switch (c) { |
||||
case 0: |
||||
return CONSTRAINT_SPACE_WORLD; |
||||
case 1: |
||||
return CONSTRAINT_SPACE_LOCAL; |
||||
case 2: |
||||
return CONSTRAINT_SPACE_POSE; |
||||
case 3: |
||||
return CONSTRAINT_SPACE_PARLOCAL; |
||||
default: |
||||
throw new IllegalArgumentException("Value: " + c + " cannot be converted to Space enum instance!"); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,397 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.Set; |
||||
import java.util.Stack; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.AnimChannel; |
||||
import com.jme3.animation.AnimControl; |
||||
import com.jme3.animation.Animation; |
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.BoneTrack; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.animation.SpatialTrack; |
||||
import com.jme3.animation.Track; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
import com.jme3.util.TempVars; |
||||
|
||||
/** |
||||
* A node that represents either spatial or bone in constraint simulation. The |
||||
* node is applied its translation, rotation and scale for each frame of its |
||||
* animation. Then the constraints are applied that will eventually alter it. |
||||
* After that the feature's transformation is stored in VirtualTrack which is |
||||
* converted to new bone or spatial track at the very end. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class SimulationNode { |
||||
private static final Logger LOGGER = Logger.getLogger(SimulationNode.class.getName()); |
||||
|
||||
private Long featureOMA; |
||||
/** The blender context. */ |
||||
private BlenderContext blenderContext; |
||||
/** The name of the node (for debugging purposes). */ |
||||
private String name; |
||||
/** A list of children for the node (either bones or child spatials). */ |
||||
private List<SimulationNode> children = new ArrayList<SimulationNode>(); |
||||
/** A list of node's animations. */ |
||||
private List<Animation> animations; |
||||
|
||||
/** The nodes spatial (if null then the boneContext should be set). */ |
||||
private Spatial spatial; |
||||
/** The skeleton of the bone (not null if the node simulated the bone). */ |
||||
private Skeleton skeleton; |
||||
/** Animation controller for the node's feature. */ |
||||
private AnimControl animControl; |
||||
|
||||
/** |
||||
* The star transform of a spatial. Needed to properly reset the spatial to |
||||
* its start position. |
||||
*/ |
||||
private Transform spatialStartTransform; |
||||
/** Star transformations for bones. Needed to properly reset the bones. */ |
||||
private Map<Bone, Transform> boneStartTransforms; |
||||
|
||||
/** |
||||
* Builds the nodes tree for the given feature. The feature (bone or |
||||
* spatial) is found by its OMA. The feature must be a root bone or a root |
||||
* spatial. |
||||
* |
||||
* @param featureOMA |
||||
* the OMA of either bone or spatial |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public SimulationNode(Long featureOMA, BlenderContext blenderContext) { |
||||
this(featureOMA, blenderContext, true); |
||||
} |
||||
|
||||
/** |
||||
* Creates the node for the feature. |
||||
* |
||||
* @param featureOMA |
||||
* the OMA of either bone or spatial |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @param rootNode |
||||
* indicates if the feature is a root bone or root spatial or not |
||||
*/ |
||||
private SimulationNode(Long featureOMA, BlenderContext blenderContext, boolean rootNode) { |
||||
this.featureOMA = featureOMA; |
||||
this.blenderContext = blenderContext; |
||||
Node spatial = (Node) blenderContext.getLoadedFeature(featureOMA, LoadedDataType.FEATURE); |
||||
if (blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, spatial) != null) { |
||||
skeleton = blenderContext.getSkeleton(featureOMA); |
||||
|
||||
Node nodeWithAnimationControl = blenderContext.getControlledNode(skeleton); |
||||
animControl = nodeWithAnimationControl.getControl(AnimControl.class); |
||||
|
||||
boneStartTransforms = new HashMap<Bone, Transform>(); |
||||
for (int i = 0; i < skeleton.getBoneCount(); ++i) { |
||||
Bone bone = skeleton.getBone(i); |
||||
boneStartTransforms.put(bone, new Transform(bone.getBindPosition(), bone.getBindRotation(), bone.getBindScale())); |
||||
} |
||||
} else { |
||||
if (rootNode && spatial.getParent() != null) { |
||||
throw new IllegalStateException("Given spatial must be a root node!"); |
||||
} |
||||
this.spatial = spatial; |
||||
spatialStartTransform = spatial.getLocalTransform().clone(); |
||||
} |
||||
|
||||
name = '>' + spatial.getName() + '<'; |
||||
|
||||
// add children nodes
|
||||
if (skeleton != null) { |
||||
Node node = blenderContext.getControlledNode(skeleton); |
||||
Long animatedNodeOMA = ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue(); |
||||
animations = blenderContext.getAnimations(animatedNodeOMA); |
||||
} else { |
||||
animations = blenderContext.getAnimations(featureOMA); |
||||
for (Spatial child : spatial.getChildren()) { |
||||
if (child instanceof Node) { |
||||
children.add(new SimulationNode((Long) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, child), blenderContext, false)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Resets the node's feature to its starting transformation. |
||||
*/ |
||||
private void reset() { |
||||
if (spatial != null) { |
||||
spatial.setLocalTransform(spatialStartTransform); |
||||
for (SimulationNode child : children) { |
||||
child.reset(); |
||||
} |
||||
} else if (skeleton != null) { |
||||
for (Entry<Bone, Transform> entry : boneStartTransforms.entrySet()) { |
||||
Transform t = entry.getValue(); |
||||
entry.getKey().setBindTransforms(t.getTranslation(), t.getRotation(), t.getScale()); |
||||
entry.getKey().updateModelTransforms(); |
||||
} |
||||
skeleton.reset(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Simulates the spatial node. |
||||
*/ |
||||
private void simulateSpatial() { |
||||
List<Constraint> constraints = blenderContext.getConstraints(featureOMA); |
||||
if (constraints != null && constraints.size() > 0) { |
||||
LOGGER.fine("Simulating spatial."); |
||||
boolean applyStaticConstraints = true; |
||||
if (animations != null) { |
||||
for (Animation animation : animations) { |
||||
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation); |
||||
int maxFrame = (int) animationTimeBoundaries[0]; |
||||
float maxTime = animationTimeBoundaries[1]; |
||||
|
||||
VirtualTrack vTrack = new VirtualTrack(spatial.getName(), maxFrame, maxTime); |
||||
for (Track track : animation.getTracks()) { |
||||
for (int frame = 0; frame < maxFrame; ++frame) { |
||||
spatial.setLocalTranslation(((SpatialTrack) track).getTranslations()[frame]); |
||||
spatial.setLocalRotation(((SpatialTrack) track).getRotations()[frame]); |
||||
spatial.setLocalScale(((SpatialTrack) track).getScales()[frame]); |
||||
|
||||
for (Constraint constraint : constraints) { |
||||
constraint.apply(frame); |
||||
vTrack.setTransform(frame, spatial.getLocalTransform()); |
||||
} |
||||
} |
||||
Track newTrack = vTrack.getAsSpatialTrack(); |
||||
if (newTrack != null) { |
||||
animation.removeTrack(track); |
||||
animation.addTrack(newTrack); |
||||
} |
||||
applyStaticConstraints = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// if there are no animations then just constraint the static
|
||||
// object's transformation
|
||||
if (applyStaticConstraints) { |
||||
for (Constraint constraint : constraints) { |
||||
constraint.apply(0); |
||||
} |
||||
} |
||||
} |
||||
|
||||
for (SimulationNode child : children) { |
||||
child.simulate(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Simulates the bone node. |
||||
*/ |
||||
private void simulateSkeleton() { |
||||
LOGGER.fine("Simulating skeleton."); |
||||
Set<Long> alteredOmas = new HashSet<Long>(); |
||||
|
||||
if (animations != null) { |
||||
TempVars vars = TempVars.get(); |
||||
AnimChannel animChannel = animControl.createChannel(); |
||||
|
||||
for (Animation animation : animations) { |
||||
float[] animationTimeBoundaries = this.computeAnimationTimeBoundaries(animation); |
||||
int maxFrame = (int) animationTimeBoundaries[0]; |
||||
float maxTime = animationTimeBoundaries[1]; |
||||
|
||||
Map<Integer, VirtualTrack> tracks = new HashMap<Integer, VirtualTrack>(); |
||||
for (int frame = 0; frame < maxFrame; ++frame) { |
||||
// this MUST be done here, otherwise setting next frame of animation will
|
||||
// lead to possible errors
|
||||
this.reset(); |
||||
|
||||
// first set proper time for all bones in all the tracks ...
|
||||
for (Track track : animation.getTracks()) { |
||||
float time = ((BoneTrack) track).getTimes()[frame]; |
||||
track.setTime(time, 1, animControl, animChannel, vars); |
||||
skeleton.updateWorldVectors(); |
||||
} |
||||
|
||||
// ... and then apply constraints from the root bone to the last child ...
|
||||
Set<Long> applied = new HashSet<Long>(); |
||||
for (Bone rootBone : skeleton.getRoots()) { |
||||
// ignore the 0-indexed bone
|
||||
if (skeleton.getBoneIndex(rootBone) > 0) { |
||||
this.applyConstraints(rootBone, alteredOmas, applied, frame, new Stack<Bone>()); |
||||
} |
||||
} |
||||
|
||||
// ... add virtual tracks if necessary, for bones that were altered but had no tracks before ...
|
||||
for (Long boneOMA : alteredOmas) { |
||||
BoneContext boneContext = blenderContext.getBoneContext(boneOMA); |
||||
int boneIndex = skeleton.getBoneIndex(boneContext.getBone()); |
||||
if (!tracks.containsKey(boneIndex)) { |
||||
tracks.put(boneIndex, new VirtualTrack(boneContext.getBone().getName(), maxFrame, maxTime)); |
||||
} |
||||
} |
||||
alteredOmas.clear(); |
||||
|
||||
// ... and fill in another frame in the result track
|
||||
for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) { |
||||
Bone bone = skeleton.getBone(trackEntry.getKey()); |
||||
Transform startTransform = boneStartTransforms.get(bone); |
||||
|
||||
// track contains differences between the frame position and bind positions of bones/spatials
|
||||
Vector3f bonePositionDifference = bone.getLocalPosition().subtract(startTransform.getTranslation()); |
||||
Quaternion boneRotationDifference = startTransform.getRotation().inverse().mult(bone.getLocalRotation()).normalizeLocal(); |
||||
Vector3f boneScaleDifference = bone.getLocalScale().divide(startTransform.getScale()); |
||||
|
||||
trackEntry.getValue().setTransform(frame, new Transform(bonePositionDifference, boneRotationDifference, boneScaleDifference)); |
||||
} |
||||
} |
||||
|
||||
for (Entry<Integer, VirtualTrack> trackEntry : tracks.entrySet()) { |
||||
Track newTrack = trackEntry.getValue().getAsBoneTrack(trackEntry.getKey()); |
||||
if (newTrack != null) { |
||||
boolean trackReplaced = false; |
||||
for (Track track : animation.getTracks()) { |
||||
if (((BoneTrack) track).getTargetBoneIndex() == trackEntry.getKey().intValue()) { |
||||
animation.removeTrack(track); |
||||
animation.addTrack(newTrack); |
||||
trackReplaced = true; |
||||
break; |
||||
} |
||||
} |
||||
if (!trackReplaced) { |
||||
animation.addTrack(newTrack); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
vars.release(); |
||||
animControl.clearChannels(); |
||||
this.reset(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Applies constraints to the given bone and its children. |
||||
* The goal is to apply constraint from root bone to the last child. |
||||
* @param bone |
||||
* the bone whose constraints will be applied |
||||
* @param alteredOmas |
||||
* the set of OMAS of the altered bones (is populated if necessary) |
||||
* @param frame |
||||
* the current frame of the animation |
||||
* @param bonesStack |
||||
* the stack of bones used to avoid infinite loops while applying constraints |
||||
*/ |
||||
private void applyConstraints(Bone bone, Set<Long> alteredOmas, Set<Long> applied, int frame, Stack<Bone> bonesStack) { |
||||
if (!bonesStack.contains(bone)) { |
||||
bonesStack.push(bone); |
||||
BoneContext boneContext = blenderContext.getBoneContext(bone); |
||||
if (!applied.contains(boneContext.getBoneOma())) { |
||||
List<Constraint> constraints = this.findConstraints(boneContext.getBoneOma(), blenderContext); |
||||
if (constraints != null && constraints.size() > 0) { |
||||
for (Constraint constraint : constraints) { |
||||
if (constraint.getTargetOMA() != null && constraint.getTargetOMA() > 0L) { |
||||
// first apply constraints of the target bone
|
||||
BoneContext targetBone = blenderContext.getBoneContext(constraint.getTargetOMA()); |
||||
this.applyConstraints(targetBone.getBone(), alteredOmas, applied, frame, bonesStack); |
||||
} |
||||
constraint.apply(frame); |
||||
if (constraint.getAlteredOmas() != null) { |
||||
alteredOmas.addAll(constraint.getAlteredOmas()); |
||||
} |
||||
alteredOmas.add(boneContext.getBoneOma()); |
||||
} |
||||
} |
||||
applied.add(boneContext.getBoneOma()); |
||||
} |
||||
|
||||
List<Bone> children = bone.getChildren(); |
||||
if (children != null && children.size() > 0) { |
||||
for (Bone child : bone.getChildren()) { |
||||
this.applyConstraints(child, alteredOmas, applied, frame, bonesStack); |
||||
} |
||||
} |
||||
bonesStack.pop(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Simulates the node. |
||||
*/ |
||||
public void simulate() { |
||||
this.reset(); |
||||
if (spatial != null) { |
||||
this.simulateSpatial(); |
||||
} else { |
||||
this.simulateSkeleton(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Computes the maximum frame and time for the animation. Different tracks |
||||
* can have different lengths so here the maximum one is being found. |
||||
* |
||||
* @param animation |
||||
* the animation |
||||
* @return maximum frame and time of the animation |
||||
*/ |
||||
private float[] computeAnimationTimeBoundaries(Animation animation) { |
||||
int maxFrame = Integer.MIN_VALUE; |
||||
float maxTime = -Float.MAX_VALUE; |
||||
for (Track track : animation.getTracks()) { |
||||
if (track instanceof BoneTrack) { |
||||
maxFrame = Math.max(maxFrame, ((BoneTrack) track).getTranslations().length); |
||||
maxTime = Math.max(maxTime, ((BoneTrack) track).getTimes()[((BoneTrack) track).getTimes().length - 1]); |
||||
} else if (track instanceof SpatialTrack) { |
||||
maxFrame = Math.max(maxFrame, ((SpatialTrack) track).getTranslations().length); |
||||
maxTime = Math.max(maxTime, ((SpatialTrack) track).getTimes()[((SpatialTrack) track).getTimes().length - 1]); |
||||
} else { |
||||
throw new IllegalStateException("Unsupported track type for simuation: " + track); |
||||
} |
||||
} |
||||
return new float[] { maxFrame, maxTime }; |
||||
} |
||||
|
||||
/** |
||||
* Finds constraints for the node's features. |
||||
* |
||||
* @param ownerOMA |
||||
* the feature's OMA |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of feature's constraints or empty list if none were found |
||||
*/ |
||||
private List<Constraint> findConstraints(Long ownerOMA, BlenderContext blenderContext) { |
||||
List<Constraint> result = new ArrayList<Constraint>(); |
||||
List<Constraint> constraints = blenderContext.getConstraints(ownerOMA); |
||||
if (constraints != null) { |
||||
for (Constraint constraint : constraints) { |
||||
if (constraint.isImplemented() && constraint.validate() && constraint.isTrackToBeChanged()) { |
||||
result.add(constraint); |
||||
} |
||||
// TODO: add proper warnings to some map or set so that they are not logged on every frame
|
||||
} |
||||
} |
||||
return result.size() > 0 ? result : null; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return name; |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.Ipo; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* Constraint applied on the skeleton. This constraint is here only to make the |
||||
* application not crash when loads constraints applied to armature. But |
||||
* skeleton movement is not supported by jme so the constraint will never be |
||||
* applied. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class SkeletonConstraint extends Constraint { |
||||
private static final Logger LOGGER = Logger.getLogger(SkeletonConstraint.class.getName()); |
||||
|
||||
public SkeletonConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException { |
||||
super(constraintStructure, ownerOMA, influenceIpo, blenderContext); |
||||
} |
||||
|
||||
@Override |
||||
public boolean validate() { |
||||
LOGGER.warning("Constraints for skeleton are not supported."); |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public void apply(int frame) { |
||||
LOGGER.warning("Applying constraints to skeleton is not supported."); |
||||
} |
||||
|
||||
@Override |
||||
public Long getTargetOMA() { |
||||
LOGGER.warning("Constraints for skeleton are not supported."); |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.Ipo; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* Constraint applied on the spatial objects. This includes: nodes, cameras |
||||
* nodes and light nodes. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class SpatialConstraint extends Constraint { |
||||
public SpatialConstraint(Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException { |
||||
super(constraintStructure, ownerOMA, influenceIpo, blenderContext); |
||||
} |
||||
|
||||
@Override |
||||
public boolean validate() { |
||||
if (targetOMA != null) { |
||||
return blenderContext.getLoadedFeature(targetOMA, LoadedDataType.FEATURE) != null; |
||||
} |
||||
return constraintDefinition == null ? true : constraintDefinition.isTargetRequired(); |
||||
} |
||||
|
||||
@Override |
||||
public Long getTargetOMA() { |
||||
return targetOMA; |
||||
} |
||||
} |
@ -0,0 +1,165 @@ |
||||
package com.jme3.scene.plugins.blender.constraints; |
||||
|
||||
import java.util.ArrayList; |
||||
|
||||
import com.jme3.animation.BoneTrack; |
||||
import com.jme3.animation.SpatialTrack; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
|
||||
/** |
||||
* A virtual track that stores computed frames after constraints are applied. |
||||
* Not all the frames need to be inserted. If there are lacks then the class
|
||||
* will fill the gaps. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class VirtualTrack { |
||||
/** The name of the track (for debugging purposes). */ |
||||
private String name; |
||||
/** The last frame for the track. */ |
||||
public int maxFrame; |
||||
/** The max time for the track. */ |
||||
public float maxTime; |
||||
/** Translations of the track. */ |
||||
public ArrayList<Vector3f> translations; |
||||
/** Rotations of the track. */ |
||||
public ArrayList<Quaternion> rotations; |
||||
/** Scales of the track. */ |
||||
public ArrayList<Vector3f> scales; |
||||
|
||||
/** |
||||
* Constructs the object storing the maximum frame and time. |
||||
* |
||||
* @param maxFrame |
||||
* the last frame for the track |
||||
* @param maxTime |
||||
* the max time for the track |
||||
*/ |
||||
public VirtualTrack(String name, int maxFrame, float maxTime) { |
||||
this.name = name; |
||||
this.maxFrame = maxFrame; |
||||
this.maxTime = maxTime; |
||||
} |
||||
|
||||
/** |
||||
* Sets the transform for the given frame. |
||||
* |
||||
* @param frameIndex |
||||
* the frame for which the transform will be set |
||||
* @param transform |
||||
* the transformation to be set |
||||
*/ |
||||
public void setTransform(int frameIndex, Transform transform) { |
||||
if (translations == null) { |
||||
translations = this.createList(Vector3f.ZERO, frameIndex); |
||||
} |
||||
this.append(translations, Vector3f.ZERO, frameIndex - translations.size()); |
||||
translations.add(transform.getTranslation().clone()); |
||||
|
||||
if (rotations == null) { |
||||
rotations = this.createList(Quaternion.IDENTITY, frameIndex); |
||||
} |
||||
this.append(rotations, Quaternion.IDENTITY, frameIndex - rotations.size()); |
||||
rotations.add(transform.getRotation().clone()); |
||||
|
||||
if (scales == null) { |
||||
scales = this.createList(Vector3f.UNIT_XYZ, frameIndex); |
||||
} |
||||
this.append(scales, Vector3f.UNIT_XYZ, frameIndex - scales.size()); |
||||
scales.add(transform.getScale().clone()); |
||||
} |
||||
|
||||
/** |
||||
* Returns the track as a bone track. |
||||
* |
||||
* @param targetBoneIndex |
||||
* the bone index |
||||
* @return the bone track |
||||
*/ |
||||
public BoneTrack getAsBoneTrack(int targetBoneIndex) { |
||||
if (translations == null && rotations == null && scales == null) { |
||||
return null; |
||||
} |
||||
return new BoneTrack(targetBoneIndex, this.createTimes(), translations.toArray(new Vector3f[maxFrame]), rotations.toArray(new Quaternion[maxFrame]), scales.toArray(new Vector3f[maxFrame])); |
||||
} |
||||
|
||||
/** |
||||
* Returns the track as a spatial track. |
||||
* |
||||
* @return the spatial track |
||||
*/ |
||||
public SpatialTrack getAsSpatialTrack() { |
||||
if (translations == null && rotations == null && scales == null) { |
||||
return null; |
||||
} |
||||
return new SpatialTrack(this.createTimes(), translations.toArray(new Vector3f[maxFrame]), rotations.toArray(new Quaternion[maxFrame]), scales.toArray(new Vector3f[maxFrame])); |
||||
} |
||||
|
||||
/** |
||||
* The method creates times for the track based on the given maximum values. |
||||
* |
||||
* @return the times for the track |
||||
*/ |
||||
private float[] createTimes() { |
||||
float[] times = new float[maxFrame]; |
||||
float dT = maxTime / maxFrame; |
||||
float t = 0; |
||||
for (int i = 0; i < maxFrame; ++i) { |
||||
times[i] = t; |
||||
t += dT; |
||||
} |
||||
return times; |
||||
} |
||||
|
||||
/** |
||||
* Helper method that creates a list of a given size filled with given |
||||
* elements. |
||||
* |
||||
* @param element |
||||
* the element to be put into the list |
||||
* @param count |
||||
* the list size |
||||
* @return the list |
||||
*/ |
||||
private <T> ArrayList<T> createList(T element, int count) { |
||||
ArrayList<T> result = new ArrayList<T>(count); |
||||
for (int i = 0; i < count; ++i) { |
||||
result.add(element); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Appends the element to the given list. |
||||
* |
||||
* @param list |
||||
* the list where the element will be appended |
||||
* @param element |
||||
* the element to be appended |
||||
* @param count |
||||
* how many times the element will be appended |
||||
*/ |
||||
private <T> void append(ArrayList<T> list, T element, int count) { |
||||
for (int i = 0; i < count; ++i) { |
||||
list.add(element); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder result = new StringBuilder(2048); |
||||
result.append("TRACK: ").append(name).append('\n'); |
||||
if (translations != null && translations.size() > 0) { |
||||
result.append("TRANSLATIONS: ").append(translations.toString()).append('\n'); |
||||
} |
||||
if (rotations != null && rotations.size() > 0) { |
||||
result.append("ROTATIONS: ").append(rotations.toString()).append('\n'); |
||||
} |
||||
if (scales != null && scales.size() > 0) { |
||||
result.append("SCALES: ").append(scales.toString()).append('\n'); |
||||
} |
||||
return result.toString(); |
||||
} |
||||
} |
@ -0,0 +1,162 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import java.util.Set; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A base class for all constraint definitions. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public abstract class ConstraintDefinition { |
||||
protected ConstraintHelper constraintHelper; |
||||
/** Constraints flag. Used to load user's options applied to the constraint. */ |
||||
protected int flag; |
||||
/** The constraint's owner. Loaded during runtime. */ |
||||
private Object owner; |
||||
/** The blender context. */ |
||||
protected BlenderContext blenderContext; |
||||
/** The constraint's owner OMA. */ |
||||
protected Long ownerOMA; |
||||
/** Stores the OMA addresses of all features whose transform had been altered beside the constraint owner. */ |
||||
protected Set<Long> alteredOmas; |
||||
/** The variable that determines if the constraint will alter the track in any way. */ |
||||
protected boolean trackToBeChanged = true; |
||||
/** The name of the constraint. */ |
||||
protected String constraintName; |
||||
|
||||
/** |
||||
* Loads a constraint definition based on the constraint definition |
||||
* structure. |
||||
* |
||||
* @param constraintData |
||||
* the constraint definition structure |
||||
* @param ownerOMA |
||||
* the constraint's owner OMA |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public ConstraintDefinition(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
if (constraintData != null) {// Null constraint has no data
|
||||
Number flag = (Number) constraintData.getFieldValue("flag"); |
||||
if (flag != null) { |
||||
this.flag = flag.intValue(); |
||||
} |
||||
} |
||||
this.blenderContext = blenderContext; |
||||
constraintHelper = (ConstraintHelper) (blenderContext == null ? null : blenderContext.getHelper(ConstraintHelper.class)); |
||||
this.ownerOMA = ownerOMA; |
||||
} |
||||
|
||||
public void setConstraintName(String constraintName) { |
||||
this.constraintName = constraintName; |
||||
} |
||||
|
||||
/** |
||||
* @return determines if the definition of the constraint will change the bone in any way; in most cases |
||||
* it is possible to tell that even before the constraint baking simulation is started, so we can discard such bones from constraint |
||||
* computing to improve the computation speed and lower the computations complexity |
||||
*/ |
||||
public boolean isTrackToBeChanged() { |
||||
return trackToBeChanged; |
||||
} |
||||
|
||||
/** |
||||
* @return determines if this constraint definition requires a defined target or not |
||||
*/ |
||||
public abstract boolean isTargetRequired(); |
||||
|
||||
/** |
||||
* This method is here because we have no guarantee that the owner is loaded |
||||
* when constraint is being created. So use it to get the owner when it is |
||||
* needed for computations. |
||||
* |
||||
* @return the owner of the constraint or null if none is set |
||||
*/ |
||||
protected Object getOwner() { |
||||
if (ownerOMA != null && owner == null) { |
||||
owner = blenderContext.getLoadedFeature(ownerOMA, LoadedDataType.FEATURE); |
||||
if (owner == null) { |
||||
throw new IllegalStateException("Cannot load constraint's owner for constraint type: " + this.getClass().getName()); |
||||
} |
||||
} |
||||
return owner; |
||||
} |
||||
|
||||
/** |
||||
* The method gets the owner's transformation. The owner can be either bone or spatial. |
||||
* @param ownerSpace |
||||
* the space in which the computed transformation is given |
||||
* @return the constraint owner's transformation |
||||
*/ |
||||
protected Transform getOwnerTransform(Space ownerSpace) { |
||||
if (this.getOwner() instanceof Bone) { |
||||
BoneContext boneContext = blenderContext.getBoneContext(ownerOMA); |
||||
return constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace); |
||||
} |
||||
return constraintHelper.getTransform(ownerOMA, null, ownerSpace); |
||||
} |
||||
|
||||
/** |
||||
* The method applies the given transformation to the owner. |
||||
* @param ownerTransform |
||||
* the transformation to apply to the owner |
||||
* @param ownerSpace |
||||
* the space that defines which owner's transformation (ie. global, local, etc. will be set) |
||||
*/ |
||||
protected void applyOwnerTransform(Transform ownerTransform, Space ownerSpace) { |
||||
if (this.getOwner() instanceof Bone) { |
||||
BoneContext boneContext = blenderContext.getBoneContext(ownerOMA); |
||||
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), ownerSpace, ownerTransform); |
||||
} else { |
||||
constraintHelper.applyTransform(ownerOMA, null, ownerSpace, ownerTransform); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the definition is implemented and <b>false</b> |
||||
* otherwise |
||||
*/ |
||||
public boolean isImplemented() { |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @return a list of all OMAs of the features that the constraint had altered beside its owner |
||||
*/ |
||||
public Set<Long> getAlteredOmas() { |
||||
return alteredOmas; |
||||
} |
||||
|
||||
/** |
||||
* @return the type name of the constraint |
||||
*/ |
||||
public abstract String getConstraintTypeName(); |
||||
|
||||
/** |
||||
* Bakes the constraint for the current feature (bone or spatial) position. |
||||
* |
||||
* @param ownerSpace |
||||
* the space where owner transform will be evaluated in |
||||
* @param targetSpace |
||||
* the space where target transform will be evaluated in |
||||
* @param targetTransform |
||||
* the target transform used by some of the constraints |
||||
* @param influence |
||||
* the influence of the constraint from range [0; 1] |
||||
*/ |
||||
public abstract void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence); |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.getConstraintTypeName(); |
||||
} |
||||
} |
@ -0,0 +1,84 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Dist limit' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionDistLimit extends ConstraintDefinition { |
||||
private static final int LIMITDIST_INSIDE = 0; |
||||
private static final int LIMITDIST_OUTSIDE = 1; |
||||
private static final int LIMITDIST_ONSURFACE = 2; |
||||
|
||||
protected int mode; |
||||
protected float dist; |
||||
|
||||
public ConstraintDefinitionDistLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
mode = ((Number) constraintData.getFieldValue("mode")).intValue(); |
||||
dist = ((Number) constraintData.getFieldValue("dist")).floatValue(); |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null && blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)) { |
||||
// distance limit does not work on bones who are connected to their parent
|
||||
return; |
||||
} |
||||
if (influence == 0 || targetTransform == null) { |
||||
return;// no need to do anything
|
||||
} |
||||
|
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
Vector3f v = ownerTransform.getTranslation().subtract(targetTransform.getTranslation()); |
||||
float currentDistance = v.length(); |
||||
switch (mode) { |
||||
case LIMITDIST_INSIDE: |
||||
if (currentDistance >= dist) { |
||||
v.normalizeLocal(); |
||||
v.multLocal(dist + (currentDistance - dist) * (1.0f - influence)); |
||||
ownerTransform.getTranslation().set(v.addLocal(targetTransform.getTranslation())); |
||||
} |
||||
break; |
||||
case LIMITDIST_ONSURFACE: |
||||
if (currentDistance > dist) { |
||||
v.normalizeLocal(); |
||||
v.multLocal(dist + (currentDistance - dist) * (1.0f - influence)); |
||||
ownerTransform.getTranslation().set(v.addLocal(targetTransform.getTranslation())); |
||||
} else if (currentDistance < dist) { |
||||
v.normalizeLocal().multLocal(dist * influence); |
||||
ownerTransform.getTranslation().set(targetTransform.getTranslation().add(v)); |
||||
} |
||||
break; |
||||
case LIMITDIST_OUTSIDE: |
||||
if (currentDistance <= dist) { |
||||
v = targetTransform.getTranslation().subtract(ownerTransform.getTranslation()).normalizeLocal().multLocal(dist * influence); |
||||
ownerTransform.getTranslation().set(targetTransform.getTranslation().add(v)); |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown distance limit constraint mode: " + mode); |
||||
} |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Limit distance"; |
||||
} |
||||
} |
@ -0,0 +1,126 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import java.lang.reflect.InvocationTargetException; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
public class ConstraintDefinitionFactory { |
||||
private static final Map<String, Class<? extends ConstraintDefinition>> CONSTRAINT_CLASSES = new HashMap<String, Class<? extends ConstraintDefinition>>(); |
||||
static { |
||||
CONSTRAINT_CLASSES.put("bDistLimitConstraint", ConstraintDefinitionDistLimit.class); |
||||
CONSTRAINT_CLASSES.put("bLocateLikeConstraint", ConstraintDefinitionLocLike.class); |
||||
CONSTRAINT_CLASSES.put("bLocLimitConstraint", ConstraintDefinitionLocLimit.class); |
||||
CONSTRAINT_CLASSES.put("bNullConstraint", ConstraintDefinitionNull.class); |
||||
CONSTRAINT_CLASSES.put("bRotateLikeConstraint", ConstraintDefinitionRotLike.class); |
||||
CONSTRAINT_CLASSES.put("bRotLimitConstraint", ConstraintDefinitionRotLimit.class); |
||||
CONSTRAINT_CLASSES.put("bSizeLikeConstraint", ConstraintDefinitionSizeLike.class); |
||||
CONSTRAINT_CLASSES.put("bSizeLimitConstraint", ConstraintDefinitionSizeLimit.class); |
||||
CONSTRAINT_CLASSES.put("bKinematicConstraint", ConstraintDefinitionIK.class); |
||||
CONSTRAINT_CLASSES.put("bTransLikeConstraint", ConstraintDefinitionTransLike.class);// since blender 2.51
|
||||
CONSTRAINT_CLASSES.put("bSameVolumeConstraint", ConstraintDefinitionMaintainVolume.class);// since blender 2.53
|
||||
} |
||||
|
||||
private static final Map<String, String> UNSUPPORTED_CONSTRAINTS = new HashMap<String, String>(); |
||||
static { |
||||
UNSUPPORTED_CONSTRAINTS.put("bActionConstraint", "Action"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bChildOfConstraint", "Child of"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bClampToConstraint", "Clamp to"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bFollowPathConstraint", "Follow path"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bLockTrackConstraint", "Lock track"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bMinMaxConstraint", "Min max"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bPythonConstraint", "Python/Script"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bRigidBodyJointConstraint", "Rigid body joint"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bShrinkWrapConstraint", "Shrinkwrap"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bStretchToConstraint", "Stretch to"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bTransformConstraint", "Transform"); |
||||
// Blender 2.50+
|
||||
UNSUPPORTED_CONSTRAINTS.put("bSplineIKConstraint", "Spline inverse kinematics"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bDampTrackConstraint", "Damp track"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bPivotConstraint", "Pivot"); |
||||
// Blender 2.56+
|
||||
UNSUPPORTED_CONSTRAINTS.put("bTrackToConstraint", "Track to"); |
||||
// Blender 2.62+
|
||||
UNSUPPORTED_CONSTRAINTS.put("bCameraSolverConstraint", "Camera solver"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bObjectSolverConstraint", "Object solver"); |
||||
UNSUPPORTED_CONSTRAINTS.put("bFollowTrackConstraint", "Follow track"); |
||||
} |
||||
|
||||
/** |
||||
* This method creates the constraint instance. |
||||
* |
||||
* @param constraintStructure |
||||
* the constraint's structure (bConstraint clss in blender 2.49). |
||||
* If the value is null the NullConstraint is created. |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public static ConstraintDefinition createConstraintDefinition(Structure constraintStructure, String constraintName, Long ownerOMA, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (constraintStructure == null) { |
||||
return new ConstraintDefinitionNull(null, ownerOMA, blenderContext); |
||||
} |
||||
String constraintClassName = constraintStructure.getType(); |
||||
Class<? extends ConstraintDefinition> constraintDefinitionClass = CONSTRAINT_CLASSES.get(constraintClassName); |
||||
if (constraintDefinitionClass != null) { |
||||
try { |
||||
ConstraintDefinition def = (ConstraintDefinition) constraintDefinitionClass.getDeclaredConstructors()[0].newInstance(constraintStructure, ownerOMA, blenderContext); |
||||
def.setConstraintName(constraintName); |
||||
return def; |
||||
} catch (IllegalArgumentException e) { |
||||
throw new BlenderFileException(e.getLocalizedMessage(), e); |
||||
} catch (SecurityException e) { |
||||
throw new BlenderFileException(e.getLocalizedMessage(), e); |
||||
} catch (InstantiationException e) { |
||||
throw new BlenderFileException(e.getLocalizedMessage(), e); |
||||
} catch (IllegalAccessException e) { |
||||
throw new BlenderFileException(e.getLocalizedMessage(), e); |
||||
} catch (InvocationTargetException e) { |
||||
throw new BlenderFileException(e.getLocalizedMessage(), e); |
||||
} |
||||
} else { |
||||
String unsupportedConstraintClassName = UNSUPPORTED_CONSTRAINTS.get(constraintClassName); |
||||
if (unsupportedConstraintClassName != null) { |
||||
return new UnsupportedConstraintDefinition(unsupportedConstraintClassName); |
||||
} else { |
||||
throw new BlenderFileException("Unknown constraint type: " + constraintClassName); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,236 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
|
||||
import org.ejml.simple.SimpleMatrix; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.math.DQuaternion; |
||||
import com.jme3.scene.plugins.blender.math.DTransform; |
||||
import com.jme3.scene.plugins.blender.math.Matrix; |
||||
import com.jme3.scene.plugins.blender.math.Vector3d; |
||||
|
||||
/** |
||||
* A definiotion of a Inverse Kinematics constraint. This implementation uses Jacobian pseudoinverse algorithm. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class ConstraintDefinitionIK extends ConstraintDefinition { |
||||
private static final float MIN_DISTANCE = 0.001f; |
||||
private static final float MIN_ANGLE_CHANGE = 0.001f; |
||||
private static final int FLAG_USE_TAIL = 0x01; |
||||
private static final int FLAG_POSITION = 0x20; |
||||
|
||||
private BonesChain bones; |
||||
/** The number of affected bones. Zero means that all parent bones of the current bone should take part in baking. */ |
||||
private int bonesAffected; |
||||
/** Indicates if the tail of the bone should be used or not. */ |
||||
private boolean useTail; |
||||
/** The amount of iterations of the algorithm. */ |
||||
private int iterations; |
||||
/** The count of bones' chain. */ |
||||
private int bonesCount = -1; |
||||
|
||||
public ConstraintDefinitionIK(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
bonesAffected = ((Number) constraintData.getFieldValue("rootbone")).intValue(); |
||||
iterations = ((Number) constraintData.getFieldValue("iterations")).intValue(); |
||||
useTail = (flag & FLAG_USE_TAIL) != 0; |
||||
|
||||
if ((flag & FLAG_POSITION) == 0) { |
||||
trackToBeChanged = false; |
||||
} |
||||
|
||||
if (trackToBeChanged) { |
||||
alteredOmas = new HashSet<Long>(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Below are the variables that only need to be allocated once for IK constraint instance. |
||||
*/ |
||||
/** Temporal quaternion. */ |
||||
private DQuaternion tempDQuaternion = new DQuaternion(); |
||||
/** Temporal matrix column. */ |
||||
private Vector3d col = new Vector3d(); |
||||
/** Effector's position change. */ |
||||
private Matrix deltaP = new Matrix(3, 1); |
||||
/** The current target position. */ |
||||
private Vector3d target = new Vector3d(); |
||||
/** Rotation vectors for each joint (allocated when we know the size of a bones' chain. */ |
||||
private Vector3d[] rotationVectors; |
||||
/** The Jacobian matrix. Allocated when the bones' chain size is known. */ |
||||
private Matrix J; |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || !trackToBeChanged || targetTransform == null || bonesCount == 0) { |
||||
return;// no need to do anything
|
||||
} |
||||
|
||||
if (bones == null) { |
||||
bones = new BonesChain((Bone) this.getOwner(), useTail, bonesAffected, alteredOmas, blenderContext); |
||||
} |
||||
if (bones.size() == 0) { |
||||
bonesCount = 0; |
||||
return;// no need to do anything
|
||||
} |
||||
double distanceFromTarget = Double.MAX_VALUE; |
||||
target.set(targetTransform.getTranslation().x, targetTransform.getTranslation().y, targetTransform.getTranslation().z); |
||||
|
||||
if (bonesCount < 0) { |
||||
bonesCount = bones.size(); |
||||
rotationVectors = new Vector3d[bonesCount]; |
||||
for (int i = 0; i < bonesCount; ++i) { |
||||
rotationVectors[i] = new Vector3d(); |
||||
} |
||||
J = new Matrix(3, bonesCount); |
||||
} |
||||
|
||||
BoneContext topBone = bones.get(0); |
||||
for (int i = 0; i < iterations; ++i) { |
||||
DTransform topBoneTransform = bones.getWorldTransform(topBone); |
||||
Vector3d e = topBoneTransform.getTranslation().add(topBoneTransform.getRotation().mult(Vector3d.UNIT_Y).multLocal(topBone.getLength()));// effector
|
||||
distanceFromTarget = e.distance(target); |
||||
if (distanceFromTarget <= MIN_DISTANCE) { |
||||
break; |
||||
} |
||||
|
||||
deltaP.setColumn(0, 0, target.x - e.x, target.y - e.y, target.z - e.z); |
||||
int column = 0; |
||||
for (BoneContext boneContext : bones) { |
||||
DTransform boneWorldTransform = bones.getWorldTransform(boneContext); |
||||
Vector3d j = boneWorldTransform.getTranslation(); // current join position
|
||||
Vector3d vectorFromJointToEffector = e.subtract(j); |
||||
vectorFromJointToEffector.cross(target.subtract(j), rotationVectors[column]).normalizeLocal(); |
||||
rotationVectors[column].cross(vectorFromJointToEffector, col); |
||||
J.setColumn(col, column++); |
||||
} |
||||
Matrix J_1 = J.pseudoinverse(); |
||||
|
||||
SimpleMatrix deltaThetas = J_1.mult(deltaP); |
||||
if (deltaThetas.elementMaxAbs() < MIN_ANGLE_CHANGE) { |
||||
break; |
||||
} |
||||
for (int j = 0; j < deltaThetas.numRows(); ++j) { |
||||
double angle = deltaThetas.get(j, 0); |
||||
Vector3d rotationVector = rotationVectors[j]; |
||||
|
||||
tempDQuaternion.fromAngleAxis(angle, rotationVector); |
||||
BoneContext boneContext = bones.get(j); |
||||
Bone bone = boneContext.getBone(); |
||||
if (bone.equals(this.getOwner())) { |
||||
if (boneContext.isLockX()) { |
||||
tempDQuaternion.set(0, tempDQuaternion.getY(), tempDQuaternion.getZ(), tempDQuaternion.getW()); |
||||
} |
||||
if (boneContext.isLockY()) { |
||||
tempDQuaternion.set(tempDQuaternion.getX(), 0, tempDQuaternion.getZ(), tempDQuaternion.getW()); |
||||
} |
||||
if (boneContext.isLockZ()) { |
||||
tempDQuaternion.set(tempDQuaternion.getX(), tempDQuaternion.getY(), 0, tempDQuaternion.getW()); |
||||
} |
||||
} |
||||
|
||||
DTransform boneTransform = bones.getWorldTransform(boneContext); |
||||
boneTransform.getRotation().set(tempDQuaternion.mult(boneTransform.getRotation())); |
||||
bones.setWorldTransform(boneContext, boneTransform); |
||||
} |
||||
} |
||||
|
||||
// applying the results
|
||||
for (int i = bonesCount - 1; i >= 0; --i) { |
||||
BoneContext boneContext = bones.get(i); |
||||
DTransform transform = bones.getWorldTransform(boneContext); |
||||
constraintHelper.applyTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD, transform.toTransform()); |
||||
} |
||||
bones = null;// need to reload them again
|
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Inverse kinematics"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Loaded bones' chain. This class allows to operate on transform matrices that use double precision in computations. |
||||
* Only the final result is being transformed to single precision numbers. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class BonesChain extends ArrayList<BoneContext> { |
||||
private static final long serialVersionUID = -1850524345643600718L; |
||||
|
||||
private List<Matrix> localBonesMatrices = new ArrayList<Matrix>(); |
||||
|
||||
public BonesChain(Bone bone, boolean useTail, int bonesAffected, Collection<Long> alteredOmas, BlenderContext blenderContext) { |
||||
if (bone != null) { |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
if (!useTail) { |
||||
bone = bone.getParent(); |
||||
} |
||||
while (bone != null && (bonesAffected <= 0 || this.size() < bonesAffected)) { |
||||
BoneContext boneContext = blenderContext.getBoneContext(bone); |
||||
this.add(boneContext); |
||||
alteredOmas.add(boneContext.getBoneOma()); |
||||
|
||||
Transform transform = constraintHelper.getTransform(boneContext.getArmatureObjectOMA(), boneContext.getBone().getName(), Space.CONSTRAINT_SPACE_WORLD); |
||||
localBonesMatrices.add(new DTransform(transform).toMatrix()); |
||||
|
||||
bone = bone.getParent(); |
||||
} |
||||
|
||||
if(localBonesMatrices.size() > 0) { |
||||
// making the matrices describe the local transformation
|
||||
Matrix parentWorldMatrix = localBonesMatrices.get(localBonesMatrices.size() - 1); |
||||
for(int i=localBonesMatrices.size() - 2;i>=0;--i) { |
||||
SimpleMatrix m = parentWorldMatrix.invert().mult(localBonesMatrices.get(i)); |
||||
parentWorldMatrix = localBonesMatrices.get(i); |
||||
localBonesMatrices.set(i, new Matrix(m)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
public DTransform getWorldTransform(BoneContext bone) { |
||||
int index = this.indexOf(bone); |
||||
return this.getWorldMatrix(index).toTransform(); |
||||
} |
||||
|
||||
public void setWorldTransform(BoneContext bone, DTransform transform) { |
||||
int index = this.indexOf(bone); |
||||
Matrix boneMatrix = transform.toMatrix(); |
||||
|
||||
if (index < this.size() - 1) { |
||||
// computing the current bone local transform
|
||||
Matrix parentWorldMatrix = this.getWorldMatrix(index + 1); |
||||
SimpleMatrix m = parentWorldMatrix.invert().mult(boneMatrix); |
||||
boneMatrix = new Matrix(m); |
||||
} |
||||
localBonesMatrices.set(index, boneMatrix); |
||||
} |
||||
|
||||
public Matrix getWorldMatrix(int index) { |
||||
if (index == this.size() - 1) { |
||||
return new Matrix(localBonesMatrices.get(this.size() - 1)); |
||||
} |
||||
|
||||
SimpleMatrix result = this.getWorldMatrix(index + 1); |
||||
result = result.mult(localBonesMatrices.get(index)); |
||||
return new Matrix(result); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Loc like' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionLocLike extends ConstraintDefinition { |
||||
private static final int LOCLIKE_X = 0x01; |
||||
private static final int LOCLIKE_Y = 0x02; |
||||
private static final int LOCLIKE_Z = 0x04; |
||||
// protected static final int LOCLIKE_TIP = 0x08;//this is deprecated in
|
||||
// blender
|
||||
private static final int LOCLIKE_X_INVERT = 0x10; |
||||
private static final int LOCLIKE_Y_INVERT = 0x20; |
||||
private static final int LOCLIKE_Z_INVERT = 0x40; |
||||
private static final int LOCLIKE_OFFSET = 0x80; |
||||
|
||||
public ConstraintDefinitionLocLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
// swapping Y and X limits flag in the bitwise flag
|
||||
int y = flag & LOCLIKE_Y; |
||||
int invY = flag & LOCLIKE_Y_INVERT; |
||||
int z = flag & LOCLIKE_Z; |
||||
int invZ = flag & LOCLIKE_Z_INVERT; |
||||
// clear the other flags to swap them
|
||||
flag &= LOCLIKE_X | LOCLIKE_X_INVERT | LOCLIKE_OFFSET; |
||||
|
||||
flag |= y << 1; |
||||
flag |= invY << 1; |
||||
flag |= z >> 1; |
||||
flag |= invZ >> 1; |
||||
|
||||
trackToBeChanged = (flag & LOCLIKE_X) != 0 || (flag & LOCLIKE_Y) != 0 || (flag & LOCLIKE_Z) != 0; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTrackToBeChanged() { |
||||
// location copy does not work on bones who are connected to their parent
|
||||
return trackToBeChanged && !(this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null && blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)); |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || targetTransform == null || !this.isTrackToBeChanged()) { |
||||
return; |
||||
} |
||||
|
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
Vector3f ownerLocation = ownerTransform.getTranslation(); |
||||
Vector3f targetLocation = targetTransform.getTranslation(); |
||||
|
||||
Vector3f startLocation = ownerTransform.getTranslation().clone(); |
||||
Vector3f offset = Vector3f.ZERO; |
||||
if ((flag & LOCLIKE_OFFSET) != 0) {// we add the original location to the copied location
|
||||
offset = startLocation; |
||||
} |
||||
|
||||
if ((flag & LOCLIKE_X) != 0) { |
||||
ownerLocation.x = targetLocation.x; |
||||
if ((flag & LOCLIKE_X_INVERT) != 0) { |
||||
ownerLocation.x = -ownerLocation.x; |
||||
} |
||||
} |
||||
if ((flag & LOCLIKE_Y) != 0) { |
||||
ownerLocation.y = targetLocation.y; |
||||
if ((flag & LOCLIKE_Y_INVERT) != 0) { |
||||
ownerLocation.y = -ownerLocation.y; |
||||
} |
||||
} |
||||
if ((flag & LOCLIKE_Z) != 0) { |
||||
ownerLocation.z = targetLocation.z; |
||||
if ((flag & LOCLIKE_Z_INVERT) != 0) { |
||||
ownerLocation.z = -ownerLocation.z; |
||||
} |
||||
} |
||||
ownerLocation.addLocal(offset); |
||||
|
||||
if (influence < 1.0f) { |
||||
startLocation.subtractLocal(ownerLocation).normalizeLocal().mult(influence); |
||||
ownerLocation.addLocal(startLocation); |
||||
} |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Copy location"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,106 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Loc limit' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionLocLimit extends ConstraintDefinition { |
||||
private static final int LIMIT_XMIN = 0x01; |
||||
private static final int LIMIT_XMAX = 0x02; |
||||
private static final int LIMIT_YMIN = 0x04; |
||||
private static final int LIMIT_YMAX = 0x08; |
||||
private static final int LIMIT_ZMIN = 0x10; |
||||
private static final int LIMIT_ZMAX = 0x20; |
||||
|
||||
protected float[][] limits = new float[3][2]; |
||||
|
||||
public ConstraintDefinitionLocLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue(); |
||||
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue(); |
||||
limits[2][0] = -((Number) constraintData.getFieldValue("ymin")).floatValue(); |
||||
limits[2][1] = -((Number) constraintData.getFieldValue("ymax")).floatValue(); |
||||
limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue(); |
||||
limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue(); |
||||
|
||||
// swapping Y and X limits flag in the bitwise flag
|
||||
int ymin = flag & LIMIT_YMIN; |
||||
int ymax = flag & LIMIT_YMAX; |
||||
int zmin = flag & LIMIT_ZMIN; |
||||
int zmax = flag & LIMIT_ZMAX; |
||||
flag &= LIMIT_XMIN | LIMIT_XMAX;// clear the other flags to swap
|
||||
// them
|
||||
flag |= ymin << 2; |
||||
flag |= ymax << 2; |
||||
flag |= zmin >> 2; |
||||
flag |= zmax >> 2; |
||||
} else { |
||||
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue(); |
||||
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue(); |
||||
limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue(); |
||||
limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue(); |
||||
limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue(); |
||||
limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue(); |
||||
} |
||||
|
||||
trackToBeChanged = (flag & (LIMIT_XMIN | LIMIT_XMAX | LIMIT_YMIN | LIMIT_YMAX | LIMIT_ZMIN | LIMIT_ZMAX)) != 0; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTrackToBeChanged() { |
||||
// location limit does not work on bones who are connected to their parent
|
||||
return trackToBeChanged && !(this.getOwner() instanceof Bone && ((Bone) this.getOwner()).getParent() != null && blenderContext.getBoneContext(ownerOMA).is(BoneContext.CONNECTED_TO_PARENT)); |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || !this.isTrackToBeChanged()) { |
||||
return;// no need to do anything
|
||||
} |
||||
|
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
Vector3f translation = ownerTransform.getTranslation(); |
||||
|
||||
if ((flag & LIMIT_XMIN) != 0 && translation.x < limits[0][0]) { |
||||
translation.x -= (translation.x - limits[0][0]) * influence; |
||||
} |
||||
if ((flag & LIMIT_XMAX) != 0 && translation.x > limits[0][1]) { |
||||
translation.x -= (translation.x - limits[0][1]) * influence; |
||||
} |
||||
if ((flag & LIMIT_YMIN) != 0 && translation.y < limits[1][0]) { |
||||
translation.y -= (translation.y - limits[1][0]) * influence; |
||||
} |
||||
if ((flag & LIMIT_YMAX) != 0 && translation.y > limits[1][1]) { |
||||
translation.y -= (translation.y - limits[1][1]) * influence; |
||||
} |
||||
if ((flag & LIMIT_ZMIN) != 0 && translation.z < limits[2][0]) { |
||||
translation.z -= (translation.z - limits[2][0]) * influence; |
||||
} |
||||
if ((flag & LIMIT_ZMAX) != 0 && translation.z > limits[2][1]) { |
||||
translation.z -= (translation.z - limits[2][1]) * influence; |
||||
} |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Limit location"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Maintain volume' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class ConstraintDefinitionMaintainVolume extends ConstraintDefinition { |
||||
private static final int FLAG_MASK_X = 0; |
||||
private static final int FLAG_MASK_Y = 1; |
||||
private static final int FLAG_MASK_Z = 2; |
||||
|
||||
private float volume; |
||||
|
||||
public ConstraintDefinitionMaintainVolume(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
volume = (float) Math.sqrt(((Number) constraintData.getFieldValue("volume")).floatValue()); |
||||
trackToBeChanged = volume != 1 && (flag & (FLAG_MASK_X | FLAG_MASK_Y | FLAG_MASK_Z)) != 0; |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (trackToBeChanged && influence > 0) { |
||||
// the maintain volume constraint is applied directly to object's scale, so no need to do it again
|
||||
// but in case of bones we need to make computations
|
||||
if (this.getOwner() instanceof Bone) { |
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
switch (flag) { |
||||
case FLAG_MASK_X: |
||||
ownerTransform.getScale().multLocal(1, volume, volume); |
||||
break; |
||||
case FLAG_MASK_Y: |
||||
ownerTransform.getScale().multLocal(volume, 1, volume); |
||||
break; |
||||
case FLAG_MASK_Z: |
||||
ownerTransform.getScale().multLocal(volume, volume, 1); |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown flag value: " + flag); |
||||
} |
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Maintain volume"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,34 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Null' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionNull extends ConstraintDefinition { |
||||
|
||||
public ConstraintDefinitionNull(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
trackToBeChanged = false; |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
// null constraint does nothing so no need to implement this one
|
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Null"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,87 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Rot like' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionRotLike extends ConstraintDefinition { |
||||
private static final int ROTLIKE_X = 0x01; |
||||
private static final int ROTLIKE_Y = 0x02; |
||||
private static final int ROTLIKE_Z = 0x04; |
||||
private static final int ROTLIKE_X_INVERT = 0x10; |
||||
private static final int ROTLIKE_Y_INVERT = 0x20; |
||||
private static final int ROTLIKE_Z_INVERT = 0x40; |
||||
private static final int ROTLIKE_OFFSET = 0x80; |
||||
|
||||
private transient float[] ownerAngles = new float[3]; |
||||
private transient float[] targetAngles = new float[3]; |
||||
|
||||
public ConstraintDefinitionRotLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
trackToBeChanged = (flag & (ROTLIKE_X | ROTLIKE_Y | ROTLIKE_Z)) != 0; |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || targetTransform == null || !trackToBeChanged) { |
||||
return;// no need to do anything
|
||||
} |
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
Quaternion ownerRotation = ownerTransform.getRotation(); |
||||
ownerAngles = ownerRotation.toAngles(ownerAngles); |
||||
targetAngles = targetTransform.getRotation().toAngles(targetAngles); |
||||
|
||||
Quaternion startRotation = ownerRotation.clone(); |
||||
Quaternion offset = Quaternion.IDENTITY; |
||||
if ((flag & ROTLIKE_OFFSET) != 0) {// we add the original rotation to
|
||||
// the copied rotation
|
||||
offset = startRotation; |
||||
} |
||||
|
||||
if ((flag & ROTLIKE_X) != 0) { |
||||
ownerAngles[0] = targetAngles[0]; |
||||
if ((flag & ROTLIKE_X_INVERT) != 0) { |
||||
ownerAngles[0] = -ownerAngles[0]; |
||||
} |
||||
} |
||||
if ((flag & ROTLIKE_Y) != 0) { |
||||
ownerAngles[1] = targetAngles[1]; |
||||
if ((flag & ROTLIKE_Y_INVERT) != 0) { |
||||
ownerAngles[1] = -ownerAngles[1]; |
||||
} |
||||
} |
||||
if ((flag & ROTLIKE_Z) != 0) { |
||||
ownerAngles[2] = targetAngles[2]; |
||||
if ((flag & ROTLIKE_Z_INVERT) != 0) { |
||||
ownerAngles[2] = -ownerAngles[2]; |
||||
} |
||||
} |
||||
ownerRotation.fromAngles(ownerAngles).multLocal(offset); |
||||
|
||||
if (influence < 1.0f) { |
||||
// startLocation.subtractLocal(ownerLocation).normalizeLocal().mult(influence);
|
||||
// ownerLocation.addLocal(startLocation);
|
||||
// TODO
|
||||
} |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Copy rotation"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,129 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Rot limit' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionRotLimit extends ConstraintDefinition { |
||||
private static final int LIMIT_XROT = 0x01; |
||||
private static final int LIMIT_YROT = 0x02; |
||||
private static final int LIMIT_ZROT = 0x04; |
||||
|
||||
private transient float[][] limits = new float[3][2]; |
||||
private transient float[] angles = new float[3]; |
||||
|
||||
public ConstraintDefinitionRotLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue(); |
||||
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue(); |
||||
limits[2][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue(); |
||||
limits[2][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue(); |
||||
limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue(); |
||||
limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue(); |
||||
|
||||
// swapping Y and X limits flag in the bitwise flag
|
||||
int limitY = flag & LIMIT_YROT; |
||||
int limitZ = flag & LIMIT_ZROT; |
||||
flag &= LIMIT_XROT;// clear the other flags to swap them
|
||||
flag |= limitY << 1; |
||||
flag |= limitZ >> 1; |
||||
} else { |
||||
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue(); |
||||
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue(); |
||||
limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue(); |
||||
limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue(); |
||||
limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue(); |
||||
limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue(); |
||||
} |
||||
|
||||
// until blender 2.49 the rotations values were stored in degrees
|
||||
if (blenderContext.getBlenderVersion() <= 249) { |
||||
for (int i = 0; i < 3; ++i) { |
||||
limits[i][0] *= FastMath.DEG_TO_RAD; |
||||
limits[i][1] *= FastMath.DEG_TO_RAD; |
||||
} |
||||
} |
||||
|
||||
// make sure that the limits are always in range [0, 2PI)
|
||||
// TODO: left it here because it is essential to make sure all cases
|
||||
// work poperly
|
||||
// but will do it a little bit later ;)
|
||||
/* |
||||
* for (int i = 0; i < 3; ++i) { for (int j = 0; j < 2; ++j) { int |
||||
* multFactor = (int)Math.abs(limits[i][j] / FastMath.TWO_PI) ; if |
||||
* (limits[i][j] < 0) { limits[i][j] += FastMath.TWO_PI * (multFactor + |
||||
* 1); } else { limits[i][j] -= FastMath.TWO_PI * multFactor; } } //make
|
||||
* sure the lower limit is not greater than the upper one |
||||
* if(limits[i][0] > limits[i][1]) { float temp = limits[i][0]; |
||||
* limits[i][0] = limits[i][1]; limits[i][1] = temp; } } |
||||
*/ |
||||
|
||||
trackToBeChanged = (flag & (LIMIT_XROT | LIMIT_YROT | LIMIT_ZROT)) != 0; |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || !trackToBeChanged) { |
||||
return; |
||||
} |
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
ownerTransform.getRotation().toAngles(angles); |
||||
// make sure that the rotations are always in range [0, 2PI)
|
||||
// TODO: same comment as in constructor
|
||||
/* |
||||
* for (int i = 0; i < 3; ++i) { int multFactor = |
||||
* (int)Math.abs(angles[i] / FastMath.TWO_PI) ; if(angles[i] < 0) { |
||||
* angles[i] += FastMath.TWO_PI * (multFactor + 1); } else { angles[i] |
||||
* -= FastMath.TWO_PI * multFactor; } } |
||||
*/ |
||||
if ((flag & LIMIT_XROT) != 0) { |
||||
float difference = 0.0f; |
||||
if (angles[0] < limits[0][0]) { |
||||
difference = (angles[0] - limits[0][0]) * influence; |
||||
} else if (angles[0] > limits[0][1]) { |
||||
difference = (angles[0] - limits[0][1]) * influence; |
||||
} |
||||
angles[0] -= difference; |
||||
} |
||||
if ((flag & LIMIT_YROT) != 0) { |
||||
float difference = 0.0f; |
||||
if (angles[1] < limits[1][0]) { |
||||
difference = (angles[1] - limits[1][0]) * influence; |
||||
} else if (angles[1] > limits[1][1]) { |
||||
difference = (angles[1] - limits[1][1]) * influence; |
||||
} |
||||
angles[1] -= difference; |
||||
} |
||||
if ((flag & LIMIT_ZROT) != 0) { |
||||
float difference = 0.0f; |
||||
if (angles[2] < limits[2][0]) { |
||||
difference = (angles[2] - limits[2][0]) * influence; |
||||
} else if (angles[2] > limits[2][1]) { |
||||
difference = (angles[2] - limits[2][1]) * influence; |
||||
} |
||||
angles[2] -= difference; |
||||
} |
||||
ownerTransform.getRotation().fromAngles(angles); |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Limit rotation"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,74 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Size like' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionSizeLike extends ConstraintDefinition { |
||||
private static final int SIZELIKE_X = 0x01; |
||||
private static final int SIZELIKE_Y = 0x02; |
||||
private static final int SIZELIKE_Z = 0x04; |
||||
private static final int LOCLIKE_OFFSET = 0x80; |
||||
|
||||
public ConstraintDefinitionSizeLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
// swapping Y and X limits flag in the bitwise flag
|
||||
int y = flag & SIZELIKE_Y; |
||||
int z = flag & SIZELIKE_Z; |
||||
flag &= SIZELIKE_X | LOCLIKE_OFFSET;// clear the other flags to swap
|
||||
// them
|
||||
flag |= y << 1; |
||||
flag |= z >> 1; |
||||
|
||||
trackToBeChanged = (flag & (SIZELIKE_X | SIZELIKE_Y | SIZELIKE_Z)) != 0; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || targetTransform == null || !trackToBeChanged) { |
||||
return;// no need to do anything
|
||||
} |
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
Vector3f ownerScale = ownerTransform.getScale(); |
||||
Vector3f targetScale = targetTransform.getScale(); |
||||
|
||||
Vector3f offset = Vector3f.ZERO; |
||||
if ((flag & LOCLIKE_OFFSET) != 0) {// we add the original scale to the
|
||||
// copied scale
|
||||
offset = ownerScale.clone(); |
||||
} |
||||
|
||||
if ((flag & SIZELIKE_X) != 0) { |
||||
ownerScale.x = targetScale.x * influence + (1.0f - influence) * ownerScale.x; |
||||
} |
||||
if ((flag & SIZELIKE_Y) != 0) { |
||||
ownerScale.y = targetScale.y * influence + (1.0f - influence) * ownerScale.y; |
||||
} |
||||
if ((flag & SIZELIKE_Z) != 0) { |
||||
ownerScale.z = targetScale.z * influence + (1.0f - influence) * ownerScale.z; |
||||
} |
||||
ownerScale.addLocal(offset); |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Copy scale"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,96 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* This class represents 'Size limit' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ConstraintDefinitionSizeLimit extends ConstraintDefinition { |
||||
private static final int LIMIT_XMIN = 0x01; |
||||
private static final int LIMIT_XMAX = 0x02; |
||||
private static final int LIMIT_YMIN = 0x04; |
||||
private static final int LIMIT_YMAX = 0x08; |
||||
private static final int LIMIT_ZMIN = 0x10; |
||||
private static final int LIMIT_ZMAX = 0x20; |
||||
|
||||
protected transient float[][] limits = new float[3][2]; |
||||
|
||||
public ConstraintDefinitionSizeLimit(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue(); |
||||
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue(); |
||||
limits[2][0] = -((Number) constraintData.getFieldValue("ymin")).floatValue(); |
||||
limits[2][1] = -((Number) constraintData.getFieldValue("ymax")).floatValue(); |
||||
limits[1][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue(); |
||||
limits[1][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue(); |
||||
|
||||
// swapping Y and X limits flag in the bitwise flag
|
||||
int ymin = flag & LIMIT_YMIN; |
||||
int ymax = flag & LIMIT_YMAX; |
||||
int zmin = flag & LIMIT_ZMIN; |
||||
int zmax = flag & LIMIT_ZMAX; |
||||
flag &= LIMIT_XMIN | LIMIT_XMAX;// clear the other flags to swap
|
||||
// them
|
||||
flag |= ymin << 2; |
||||
flag |= ymax << 2; |
||||
flag |= zmin >> 2; |
||||
flag |= zmax >> 2; |
||||
} else { |
||||
limits[0][0] = ((Number) constraintData.getFieldValue("xmin")).floatValue(); |
||||
limits[0][1] = ((Number) constraintData.getFieldValue("xmax")).floatValue(); |
||||
limits[1][0] = ((Number) constraintData.getFieldValue("ymin")).floatValue(); |
||||
limits[1][1] = ((Number) constraintData.getFieldValue("ymax")).floatValue(); |
||||
limits[2][0] = ((Number) constraintData.getFieldValue("zmin")).floatValue(); |
||||
limits[2][1] = ((Number) constraintData.getFieldValue("zmax")).floatValue(); |
||||
} |
||||
|
||||
trackToBeChanged = (flag & (LIMIT_XMIN | LIMIT_XMAX | LIMIT_YMIN | LIMIT_YMAX | LIMIT_ZMIN | LIMIT_ZMAX)) != 0; |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || !trackToBeChanged) { |
||||
return; |
||||
} |
||||
Transform ownerTransform = this.getOwnerTransform(ownerSpace); |
||||
|
||||
Vector3f scale = ownerTransform.getScale(); |
||||
if ((flag & LIMIT_XMIN) != 0 && scale.x < limits[0][0]) { |
||||
scale.x -= (scale.x - limits[0][0]) * influence; |
||||
} |
||||
if ((flag & LIMIT_XMAX) != 0 && scale.x > limits[0][1]) { |
||||
scale.x -= (scale.x - limits[0][1]) * influence; |
||||
} |
||||
if ((flag & LIMIT_YMIN) != 0 && scale.y < limits[1][0]) { |
||||
scale.y -= (scale.y - limits[1][0]) * influence; |
||||
} |
||||
if ((flag & LIMIT_YMAX) != 0 && scale.y > limits[1][1]) { |
||||
scale.y -= (scale.y - limits[1][1]) * influence; |
||||
} |
||||
if ((flag & LIMIT_ZMIN) != 0 && scale.z < limits[2][0]) { |
||||
scale.z -= (scale.z - limits[2][0]) * influence; |
||||
} |
||||
if ((flag & LIMIT_ZMAX) != 0 && scale.z > limits[2][1]) { |
||||
scale.z -= (scale.z - limits[2][1]) * influence; |
||||
} |
||||
|
||||
this.applyOwnerTransform(ownerTransform, ownerSpace); |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Limit scale"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
import com.jme3.util.TempVars; |
||||
|
||||
/** |
||||
* This class represents 'Trans like' constraint type in blender. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class ConstraintDefinitionTransLike extends ConstraintDefinition { |
||||
private Long targetOMA; |
||||
private String subtargetName; |
||||
|
||||
public ConstraintDefinitionTransLike(Structure constraintData, Long ownerOMA, BlenderContext blenderContext) { |
||||
super(constraintData, ownerOMA, blenderContext); |
||||
Pointer pTarget = (Pointer) constraintData.getFieldValue("tar"); |
||||
targetOMA = pTarget.getOldMemoryAddress(); |
||||
Object subtarget = constraintData.getFieldValue("subtarget"); |
||||
if (subtarget != null) { |
||||
subtargetName = subtarget.toString(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
if (influence == 0 || targetTransform == null) { |
||||
return;// no need to do anything
|
||||
} |
||||
Object target = this.getTarget();// Bone or Node
|
||||
Object owner = this.getOwner();// Bone or Node
|
||||
if (!target.getClass().equals(owner.getClass())) { |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
|
||||
TempVars tempVars = TempVars.get(); |
||||
Matrix4f m = constraintHelper.toMatrix(targetTransform, tempVars.tempMat4); |
||||
tempVars.tempMat42.set(BoneContext.BONE_ARMATURE_TRANSFORMATION_MATRIX); |
||||
if (target instanceof Bone) { |
||||
tempVars.tempMat42.invertLocal(); |
||||
} |
||||
m = m.multLocal(tempVars.tempMat42); |
||||
tempVars.release(); |
||||
|
||||
targetTransform = new Transform(m.toTranslationVector(), m.toRotationQuat(), m.toScaleVector()); |
||||
} |
||||
this.applyOwnerTransform(targetTransform, ownerSpace); |
||||
} |
||||
|
||||
/** |
||||
* @return the target feature; it is either Node or Bone (vertex group subtarger is not yet supported) |
||||
*/ |
||||
private Object getTarget() { |
||||
Object target = blenderContext.getLoadedFeature(targetOMA, LoadedDataType.FEATURE); |
||||
if (subtargetName != null && blenderContext.getMarkerValue(ObjectHelper.ARMATURE_NODE_MARKER, target) != null) { |
||||
Skeleton skeleton = blenderContext.getSkeleton(targetOMA); |
||||
target = skeleton.getBone(subtargetName); |
||||
} |
||||
return target; |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return "Copy transforms"; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
package com.jme3.scene.plugins.blender.constraints.definitions; |
||||
|
||||
import com.jme3.math.Transform; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper.Space; |
||||
|
||||
/** |
||||
* This class represents a constraint that is defined by blender but not |
||||
* supported by either importer ot jme. It only wirtes down a warning when |
||||
* baking is called. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class UnsupportedConstraintDefinition extends ConstraintDefinition { |
||||
private String typeName; |
||||
|
||||
public UnsupportedConstraintDefinition(String typeName) { |
||||
super(null, null, null); |
||||
this.typeName = typeName; |
||||
trackToBeChanged = false; |
||||
} |
||||
|
||||
@Override |
||||
public void bake(Space ownerSpace, Space targetSpace, Transform targetTransform, float influence) { |
||||
} |
||||
|
||||
@Override |
||||
public boolean isImplemented() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public String getConstraintTypeName() { |
||||
return typeName; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isTargetRequired() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,172 @@ |
||||
package com.jme3.scene.plugins.blender.curves; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A class that helps to calculate the bezier curves calues. It uses doubles for performing calculations to minimize |
||||
* floating point operations errors. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BezierCurve { |
||||
private static final int IPO_CONSTANT = 0; |
||||
private static final int IPO_LINEAR = 1; |
||||
private static final int IPO_BEZIER = 2; |
||||
|
||||
public static final int X_VALUE = 0; |
||||
public static final int Y_VALUE = 1; |
||||
public static final int Z_VALUE = 2; |
||||
/** |
||||
* The type of the curve. Describes the data it modifies. |
||||
* Used in ipos calculations. |
||||
*/ |
||||
private int type; |
||||
/** The dimension of the curve. */ |
||||
private int dimension; |
||||
/** A table of the bezier points. */ |
||||
private double[][][] bezierPoints; |
||||
/** Array that stores a radius for each bezier triple. */ |
||||
private double[] radiuses; |
||||
/** Interpolation types of the bezier triples. */ |
||||
private int[] interpolations; |
||||
|
||||
public BezierCurve(final int type, final List<Structure> bezTriples, final int dimension) { |
||||
this(type, bezTriples, dimension, false); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
public BezierCurve(final int type, final List<Structure> bezTriples, final int dimension, boolean fixUpAxis) { |
||||
if (dimension != 2 && dimension != 3) { |
||||
throw new IllegalArgumentException("The dimension of the curve should be 2 or 3!"); |
||||
} |
||||
this.type = type; |
||||
this.dimension = dimension; |
||||
// first index of the bezierPoints table has the length of triples amount
|
||||
// the second index points to a table od three points of a bezier triple (handle, point, handle)
|
||||
// the third index specifies the coordinates of the specific point in a bezier triple
|
||||
bezierPoints = new double[bezTriples.size()][3][dimension]; |
||||
radiuses = new double[bezTriples.size()]; |
||||
interpolations = new int[bezTriples.size()]; |
||||
int i = 0, j, k; |
||||
for (Structure bezTriple : bezTriples) { |
||||
DynamicArray<Number> vec = (DynamicArray<Number>) bezTriple.getFieldValue("vec"); |
||||
for (j = 0; j < 3; ++j) { |
||||
for (k = 0; k < dimension; ++k) { |
||||
bezierPoints[i][j][k] = vec.get(j, k).doubleValue(); |
||||
} |
||||
if (fixUpAxis && dimension == 3) { |
||||
double temp = bezierPoints[i][j][2]; |
||||
bezierPoints[i][j][2] = -bezierPoints[i][j][1]; |
||||
bezierPoints[i][j][1] = temp; |
||||
} |
||||
} |
||||
radiuses[i] = ((Number) bezTriple.getFieldValue("radius")).floatValue(); |
||||
interpolations[i++] = ((Number) bezTriple.getFieldValue("ipo", IPO_BEZIER)).intValue(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method evaluates the data for the specified frame. The Y value is returned. |
||||
* @param frame |
||||
* the frame for which the value is being calculated |
||||
* @param valuePart |
||||
* this param specifies wheather we should return the X, Y or Z part of the result value; it should have |
||||
* one of the following values: X_VALUE - the X factor of the result Y_VALUE - the Y factor of the result |
||||
* Z_VALUE - the Z factor of the result |
||||
* @return the value of the curve |
||||
*/ |
||||
public double evaluate(int frame, int valuePart) { |
||||
for (int i = 0; i < bezierPoints.length - 1; ++i) { |
||||
if (frame >= bezierPoints[i][1][0] && frame <= bezierPoints[i + 1][1][0]) { |
||||
double t = (frame - bezierPoints[i][1][0]) / (bezierPoints[i + 1][1][0] - bezierPoints[i][1][0]); |
||||
switch (interpolations[i]) { |
||||
case IPO_BEZIER: |
||||
double oneMinusT = 1.0f - t; |
||||
double oneMinusT2 = oneMinusT * oneMinusT; |
||||
double t2 = t * t; |
||||
return bezierPoints[i][1][valuePart] * oneMinusT2 * oneMinusT + 3.0f * bezierPoints[i][2][valuePart] * t * oneMinusT2 + 3.0f * bezierPoints[i + 1][0][valuePart] * t2 * oneMinusT + bezierPoints[i + 1][1][valuePart] * t2 * t; |
||||
case IPO_LINEAR: |
||||
return (1f - t) * bezierPoints[i][1][valuePart] + t * bezierPoints[i + 1][1][valuePart]; |
||||
case IPO_CONSTANT: |
||||
return bezierPoints[i][1][valuePart]; |
||||
default: |
||||
throw new IllegalStateException("Unknown interpolation type for curve: " + interpolations[i]); |
||||
} |
||||
} |
||||
} |
||||
if (frame < bezierPoints[0][1][0]) { |
||||
return bezierPoints[0][1][1]; |
||||
} else { // frame>bezierPoints[bezierPoints.length-1][1][0]
|
||||
return bezierPoints[bezierPoints.length - 1][1][1]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the frame where last bezier triple center point of the bezier curve is located. |
||||
* @return the frame number of the last defined bezier triple point for the curve |
||||
*/ |
||||
public int getLastFrame() { |
||||
return (int) bezierPoints[bezierPoints.length - 1][1][0]; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the type of the bezier curve. The type describes the parameter that this curve modifies |
||||
* (ie. LocationX or rotationW of the feature). |
||||
* @return the type of the bezier curve |
||||
*/ |
||||
public int getType() { |
||||
return type; |
||||
} |
||||
|
||||
/** |
||||
* The method returns the radius for the required bezier triple. |
||||
* |
||||
* @param bezierTripleIndex |
||||
* index of the bezier triple |
||||
* @return radius of the required bezier triple |
||||
*/ |
||||
public double getRadius(int bezierTripleIndex) { |
||||
return radiuses[bezierTripleIndex]; |
||||
} |
||||
|
||||
/** |
||||
* This method returns a list of control points for this curve. |
||||
* @return a list of control points for this curve. |
||||
*/ |
||||
public List<Vector3f> getControlPoints() { |
||||
List<Vector3f> controlPoints = new ArrayList<Vector3f>(bezierPoints.length * 3); |
||||
for (int i = 0; i < bezierPoints.length; ++i) { |
||||
controlPoints.add(new Vector3f((float) bezierPoints[i][0][0], (float) bezierPoints[i][0][1], (float) bezierPoints[i][0][2])); |
||||
controlPoints.add(new Vector3f((float) bezierPoints[i][1][0], (float) bezierPoints[i][1][1], (float) bezierPoints[i][1][2])); |
||||
controlPoints.add(new Vector3f((float) bezierPoints[i][2][0], (float) bezierPoints[i][2][1], (float) bezierPoints[i][2][2])); |
||||
} |
||||
return controlPoints; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder sb = new StringBuilder("Bezier curve: ").append(type).append('\n'); |
||||
for (int i = 0; i < bezierPoints.length; ++i) { |
||||
sb.append(this.toStringBezTriple(i)).append('\n'); |
||||
} |
||||
return sb.toString(); |
||||
} |
||||
|
||||
/** |
||||
* This method converts the bezier triple of a specified index into text. |
||||
* @param tripleIndex |
||||
* index of the triple |
||||
* @return text representation of the triple |
||||
*/ |
||||
private String toStringBezTriple(int tripleIndex) { |
||||
if (dimension == 2) { |
||||
return "[(" + bezierPoints[tripleIndex][0][0] + ", " + bezierPoints[tripleIndex][0][1] + ") (" + bezierPoints[tripleIndex][1][0] + ", " + bezierPoints[tripleIndex][1][1] + ") (" + bezierPoints[tripleIndex][2][0] + ", " + bezierPoints[tripleIndex][2][1] + ")]"; |
||||
} else { |
||||
return "[(" + bezierPoints[tripleIndex][0][0] + ", " + bezierPoints[tripleIndex][0][1] + ", " + bezierPoints[tripleIndex][0][2] + ") (" + bezierPoints[tripleIndex][1][0] + ", " + bezierPoints[tripleIndex][1][1] + ", " + bezierPoints[tripleIndex][1][2] + ") (" + bezierPoints[tripleIndex][2][0] + ", " + bezierPoints[tripleIndex][2][1] + ", " + bezierPoints[tripleIndex][2][2] + ")]"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,159 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.curves; |
||||
|
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A class that is used in mesh calculations. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class CurvesHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(CurvesHelper.class.getName()); |
||||
|
||||
/** Minimum basis U function degree for NURBS curves and surfaces. */ |
||||
protected int minimumBasisUFunctionDegree = 4; |
||||
/** Minimum basis V function degree for NURBS curves and surfaces. */ |
||||
protected int minimumBasisVFunctionDegree = 4; |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in |
||||
* different blender versions. |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public CurvesHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
public CurvesTemporalMesh toCurve(Structure curveStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
CurvesTemporalMesh result = new CurvesTemporalMesh(curveStructure, blenderContext); |
||||
|
||||
if (blenderContext.getBlenderKey().isLoadObjectProperties()) { |
||||
LOGGER.fine("Reading custom properties."); |
||||
result.setProperties(this.loadProperties(curveStructure, blenderContext)); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method transforms the bevel along the curve. |
||||
* |
||||
* @param bevel |
||||
* the bevel to be transformed |
||||
* @param prevPos |
||||
* previous curve point |
||||
* @param currPos |
||||
* current curve point (here the center of the new bevel will be |
||||
* set) |
||||
* @param nextPos |
||||
* next curve point |
||||
* @return points of transformed bevel |
||||
*/ |
||||
protected Vector3f[] transformBevel(Vector3f[] bevel, Vector3f prevPos, Vector3f currPos, Vector3f nextPos) { |
||||
bevel = bevel.clone(); |
||||
|
||||
// currPos and directionVector define the line in 3D space
|
||||
Vector3f directionVector = prevPos != null ? currPos.subtract(prevPos) : nextPos.subtract(currPos); |
||||
directionVector.normalizeLocal(); |
||||
|
||||
// plane is described by equation: Ax + By + Cz + D = 0 where planeNormal = [A, B, C] and D = -(Ax + By + Cz)
|
||||
Vector3f planeNormal = null; |
||||
if (prevPos != null) { |
||||
planeNormal = currPos.subtract(prevPos).normalizeLocal(); |
||||
if (nextPos != null) { |
||||
planeNormal.addLocal(nextPos.subtract(currPos).normalizeLocal()).normalizeLocal(); |
||||
} |
||||
} else { |
||||
planeNormal = nextPos.subtract(currPos).normalizeLocal(); |
||||
} |
||||
float D = -planeNormal.dot(currPos);// D = -(Ax + By + Cz)
|
||||
|
||||
// now we need to compute paralell cast of each bevel point on the plane, the leading line is already known
|
||||
// parametric equation of a line: x = px + vx * t; y = py + vy * t; z = pz + vz * t
|
||||
// where p = currPos and v = directionVector
|
||||
// using x, y and z in plane equation we get value of 't' that will allow us to compute the point where plane and line cross
|
||||
float temp = planeNormal.dot(directionVector); |
||||
for (int i = 0; i < bevel.length; ++i) { |
||||
float t = -(planeNormal.dot(bevel[i]) + D) / temp; |
||||
if (fixUpAxis) { |
||||
bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, bevel[i].y + directionVector.y * t, bevel[i].z + directionVector.z * t); |
||||
} else { |
||||
bevel[i] = new Vector3f(bevel[i].x + directionVector.x * t, -bevel[i].z + directionVector.z * t, bevel[i].y + directionVector.y * t); |
||||
} |
||||
} |
||||
return bevel; |
||||
} |
||||
|
||||
/** |
||||
* This method transforms the first line of the bevel points positioning it |
||||
* on the first point of the curve. |
||||
* |
||||
* @param startingLinePoints |
||||
* the vbevel shape points |
||||
* @param firstCurvePoint |
||||
* the first curve's point |
||||
* @param secondCurvePoint |
||||
* the second curve's point |
||||
* @return points of transformed bevel |
||||
*/ |
||||
protected Vector3f[] transformToFirstLineOfBevelPoints(Vector3f[] startingLinePoints, Vector3f firstCurvePoint, Vector3f secondCurvePoint) { |
||||
Vector3f planeNormal = secondCurvePoint.subtract(firstCurvePoint).normalizeLocal(); |
||||
|
||||
float angle = FastMath.acos(planeNormal.dot(Vector3f.UNIT_X)); |
||||
Vector3f rotationVector = Vector3f.UNIT_X.cross(planeNormal).normalizeLocal(); |
||||
|
||||
Matrix4f m = new Matrix4f(); |
||||
m.setRotationQuaternion(new Quaternion().fromAngleAxis(angle, rotationVector)); |
||||
m.setTranslation(firstCurvePoint); |
||||
|
||||
Vector3f temp = new Vector3f(); |
||||
Vector3f[] verts = new Vector3f[startingLinePoints.length]; |
||||
for (int i = 0; i < verts.length; ++i) { |
||||
verts[i] = m.mult(startingLinePoints[i], temp).clone(); |
||||
} |
||||
return verts; |
||||
} |
||||
} |
@ -0,0 +1,890 @@ |
||||
package com.jme3.scene.plugins.blender.curves; |
||||
|
||||
import java.nio.FloatBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.TreeMap; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.material.RenderState.FaceCullMode; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Spline; |
||||
import com.jme3.math.Spline.SplineType; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.math.Vector4f; |
||||
import com.jme3.scene.VertexBuffer.Type; |
||||
import com.jme3.scene.mesh.IndexBuffer; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.BlenderInputStream; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialContext; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.Edge; |
||||
import com.jme3.scene.plugins.blender.meshes.Face; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.shape.Curve; |
||||
import com.jme3.scene.shape.Surface; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
/** |
||||
* A temporal mesh for curves and surfaces. It works in similar way as TemporalMesh for meshes. |
||||
* It prepares all necessary lines and faces and allows to apply modifiers just like in regular temporal mesh. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class CurvesTemporalMesh extends TemporalMesh { |
||||
private static final Logger LOGGER = Logger.getLogger(CurvesTemporalMesh.class.getName()); |
||||
|
||||
private static final int TYPE_BEZIER = 0x0001; |
||||
private static final int TYPE_NURBS = 0x0004; |
||||
|
||||
private static final int FLAG_3D = 0x0001; |
||||
private static final int FLAG_FRONT = 0x0002; |
||||
private static final int FLAG_BACK = 0x0004; |
||||
private static final int FLAG_FILL_CAPS = 0x4000; |
||||
|
||||
private static final int FLAG_SMOOTH = 0x0001; |
||||
|
||||
protected CurvesHelper curvesHelper; |
||||
protected boolean is2D; |
||||
protected boolean isFront; |
||||
protected boolean isBack; |
||||
protected boolean fillCaps; |
||||
protected float bevelStart; |
||||
protected float bevelEnd; |
||||
protected List<BezierLine> beziers = new ArrayList<BezierLine>(); |
||||
protected CurvesTemporalMesh bevelObject; |
||||
protected CurvesTemporalMesh taperObject; |
||||
/** The scale that is used if the curve is a bevel or taper curve. */ |
||||
protected Vector3f scale = new Vector3f(1, 1, 1); |
||||
|
||||
/** |
||||
* The constructor creates an empty temporal mesh. |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this will never be thrown here |
||||
*/ |
||||
protected CurvesTemporalMesh(BlenderContext blenderContext) throws BlenderFileException { |
||||
super(null, blenderContext, false); |
||||
} |
||||
|
||||
/** |
||||
* Loads the temporal mesh from the given curve structure. The mesh can be either curve or surface. |
||||
* @param curveStructure |
||||
* the structure that contains the curve/surface data |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading occur |
||||
*/ |
||||
public CurvesTemporalMesh(Structure curveStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
this(curveStructure, new Vector3f(1, 1, 1), true, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* Loads the temporal mesh from the given curve structure. The mesh can be either curve or surface. |
||||
* @param curveStructure |
||||
* the structure that contains the curve/surface data |
||||
* @param scale |
||||
* the scale used if the current curve is used as a bevel curve |
||||
* @param loadBevelAndTaper indicates if bevel and taper should be loaded (this is not needed for curves that are loaded to be used as bevel and taper) |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading occur |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private CurvesTemporalMesh(Structure curveStructure, Vector3f scale, boolean loadBevelAndTaper, BlenderContext blenderContext) throws BlenderFileException { |
||||
super(curveStructure, blenderContext, false); |
||||
name = curveStructure.getName(); |
||||
curvesHelper = blenderContext.getHelper(CurvesHelper.class); |
||||
this.scale = scale; |
||||
|
||||
int flag = ((Number) curveStructure.getFieldValue("flag")).intValue(); |
||||
is2D = (flag & FLAG_3D) == 0; |
||||
if (is2D) { |
||||
// TODO: add support for 3D flag
|
||||
LOGGER.warning("2D flag not yet supported for curves!"); |
||||
} |
||||
isFront = (flag & FLAG_FRONT) != 0; |
||||
isBack = (flag & FLAG_BACK) != 0; |
||||
fillCaps = (flag & FLAG_FILL_CAPS) != 0; |
||||
bevelStart = ((Number) curveStructure.getFieldValue("bevfac1", 0)).floatValue(); |
||||
bevelEnd = ((Number) curveStructure.getFieldValue("bevfac2", 1)).floatValue(); |
||||
if (bevelStart > bevelEnd) { |
||||
float temp = bevelStart; |
||||
bevelStart = bevelEnd; |
||||
bevelEnd = temp; |
||||
} |
||||
|
||||
LOGGER.fine("Reading nurbs (and sorting them by material)."); |
||||
Map<Number, List<Structure>> nurbs = new HashMap<Number, List<Structure>>(); |
||||
List<Structure> nurbStructures = ((Structure) curveStructure.getFieldValue("nurb")).evaluateListBase(); |
||||
for (Structure nurb : nurbStructures) { |
||||
Number matNumber = (Number) nurb.getFieldValue("mat_nr"); |
||||
List<Structure> nurbList = nurbs.get(matNumber); |
||||
if (nurbList == null) { |
||||
nurbList = new ArrayList<Structure>(); |
||||
nurbs.put(matNumber, nurbList); |
||||
} |
||||
nurbList.add(nurb); |
||||
} |
||||
|
||||
LOGGER.fine("Getting materials."); |
||||
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); |
||||
materials = materialHelper.getMaterials(curveStructure, blenderContext); |
||||
if (materials != null) { |
||||
for (MaterialContext materialContext : materials) { |
||||
materialContext.setFaceCullMode(FaceCullMode.Off); |
||||
} |
||||
} |
||||
|
||||
LOGGER.fine("Getting or creating bevel object."); |
||||
bevelObject = loadBevelAndTaper ? this.loadBevelObject(curveStructure) : null; |
||||
|
||||
LOGGER.fine("Getting taper object."); |
||||
Pointer pTaperObject = (Pointer) curveStructure.getFieldValue("taperobj"); |
||||
if (bevelObject != null && pTaperObject.isNotNull()) { |
||||
Structure taperObjectStructure = pTaperObject.fetchData().get(0); |
||||
DynamicArray<Number> scaleArray = (DynamicArray<Number>) taperObjectStructure.getFieldValue("size"); |
||||
scale = blenderContext.getBlenderKey().isFixUpAxis() ? new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue()) : new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(2).floatValue(), scaleArray.get(1).floatValue()); |
||||
Pointer pTaperStructure = (Pointer) taperObjectStructure.getFieldValue("data"); |
||||
Structure taperStructure = pTaperStructure.fetchData().get(0); |
||||
taperObject = new CurvesTemporalMesh(taperStructure, blenderContext); |
||||
} |
||||
|
||||
LOGGER.fine("Creating the result curves."); |
||||
for (Entry<Number, List<Structure>> nurbEntry : nurbs.entrySet()) { |
||||
for (Structure nurb : nurbEntry.getValue()) { |
||||
int type = ((Number) nurb.getFieldValue("type")).intValue(); |
||||
if ((type & TYPE_BEZIER) != 0) { |
||||
this.loadBezierCurve(nurb, nurbEntry.getKey().intValue()); |
||||
} else if ((type & TYPE_NURBS) != 0) { |
||||
this.loadNurbSurface(nurb, nurbEntry.getKey().intValue()); |
||||
} else { |
||||
throw new BlenderFileException("Unknown curve type: " + type); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (bevelObject != null && beziers.size() > 0) { |
||||
this.append(this.applyBevelAndTaper(this, bevelObject, taperObject, blenderContext)); |
||||
} else { |
||||
for (BezierLine bezierLine : beziers) { |
||||
int originalVerticesAmount = vertices.size(); |
||||
vertices.add(bezierLine.vertices[0]); |
||||
Vector3f v = bezierLine.vertices[1].subtract(bezierLine.vertices[0]).normalizeLocal(); |
||||
float temp = v.x; |
||||
v.x = -v.y; |
||||
v.y = temp; |
||||
v.z = 0; |
||||
normals.add(v);// this will be smoothed in the next iteration
|
||||
|
||||
for (int i = 1; i < bezierLine.vertices.length; ++i) { |
||||
vertices.add(bezierLine.vertices[i]); |
||||
edges.add(new Edge(originalVerticesAmount + i - 1, originalVerticesAmount + i, 0, false, this)); |
||||
|
||||
// generating normal for vertex at 'i'
|
||||
v = bezierLine.vertices[i].subtract(bezierLine.vertices[i - 1]).normalizeLocal(); |
||||
temp = v.x; |
||||
v.x = -v.y; |
||||
v.y = temp; |
||||
v.z = 0; |
||||
|
||||
// make the previous normal smooth
|
||||
normals.get(i - 1).addLocal(v).multLocal(0.5f).normalizeLocal(); |
||||
normals.add(v);// this will be smoothed in the next iteration
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method computes the value of a point at the certain relational distance from its beginning. |
||||
* @param alongRatio |
||||
* the relative distance along the curve; should be a value between 0 and 1 inclusive; |
||||
* if the value exceeds the boundaries it is truncated to them |
||||
* @return computed value along the curve |
||||
*/ |
||||
private Vector3f getValueAlongCurve(float alongRatio) { |
||||
alongRatio = FastMath.clamp(alongRatio, 0, 1); |
||||
Vector3f result = new Vector3f(); |
||||
float probeLength = this.getLength() * alongRatio, length = 0; |
||||
for (BezierLine bezier : beziers) { |
||||
float edgeLength = bezier.getLength(); |
||||
if (length + edgeLength >= probeLength) { |
||||
float ratioAlongEdge = (probeLength - length) / edgeLength; |
||||
return bezier.getValueAlongCurve(ratioAlongEdge); |
||||
} |
||||
length += edgeLength; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return the length of the curve |
||||
*/ |
||||
private float getLength() { |
||||
float result = 0; |
||||
for (BezierLine bezier : beziers) { |
||||
result += bezier.getLength(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The methods loads the bezier curve from the given structure. |
||||
* @param nurbStructure |
||||
* the structure containing a single curve definition |
||||
* @param materialIndex |
||||
* the index of this segment's material |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading occur |
||||
*/ |
||||
private void loadBezierCurve(Structure nurbStructure, int materialIndex) throws BlenderFileException { |
||||
Pointer pBezierTriple = (Pointer) nurbStructure.getFieldValue("bezt"); |
||||
if (pBezierTriple.isNotNull()) { |
||||
int resolution = ((Number) nurbStructure.getFieldValue("resolu")).intValue(); |
||||
boolean cyclic = (((Number) nurbStructure.getFieldValue("flagu")).intValue() & 0x01) != 0; |
||||
boolean smooth = (((Number) nurbStructure.getFieldValue("flag")).intValue() & FLAG_SMOOTH) != 0; |
||||
|
||||
// creating the curve object
|
||||
BezierCurve bezierCurve = new BezierCurve(0, pBezierTriple.fetchData(), 3, blenderContext.getBlenderKey().isFixUpAxis()); |
||||
List<Vector3f> controlPoints = bezierCurve.getControlPoints(); |
||||
|
||||
if (cyclic) { |
||||
// copy the first three points at the end
|
||||
for (int i = 0; i < 3; ++i) { |
||||
controlPoints.add(controlPoints.get(i)); |
||||
} |
||||
} |
||||
// removing the first and last handles
|
||||
controlPoints.remove(0); |
||||
controlPoints.remove(controlPoints.size() - 1); |
||||
|
||||
// creating curve
|
||||
Curve curve = new Curve(new Spline(SplineType.Bezier, controlPoints, 0, false), resolution); |
||||
|
||||
FloatBuffer vertsBuffer = (FloatBuffer) curve.getBuffer(Type.Position).getData(); |
||||
beziers.add(new BezierLine(BufferUtils.getVector3Array(vertsBuffer), materialIndex, smooth, cyclic)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method loads the NURBS curve or surface. |
||||
* @param nurb |
||||
* the NURBS data structure |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading occur |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private void loadNurbSurface(Structure nurb, int materialIndex) throws BlenderFileException { |
||||
// loading the knots
|
||||
List<Float>[] knots = new List[2]; |
||||
Pointer[] pKnots = new Pointer[] { (Pointer) nurb.getFieldValue("knotsu"), (Pointer) nurb.getFieldValue("knotsv") }; |
||||
for (int i = 0; i < knots.length; ++i) { |
||||
if (pKnots[i].isNotNull()) { |
||||
FileBlockHeader fileBlockHeader = blenderContext.getFileBlock(pKnots[i].getOldMemoryAddress()); |
||||
BlenderInputStream blenderInputStream = blenderContext.getInputStream(); |
||||
blenderInputStream.setPosition(fileBlockHeader.getBlockPosition()); |
||||
int knotsAmount = fileBlockHeader.getCount() * fileBlockHeader.getSize() / 4; |
||||
knots[i] = new ArrayList<Float>(knotsAmount); |
||||
for (int j = 0; j < knotsAmount; ++j) { |
||||
knots[i].add(Float.valueOf(blenderInputStream.readFloat())); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// loading the flags and orders (basis functions degrees)
|
||||
int flag = ((Number) nurb.getFieldValue("flag")).intValue(); |
||||
boolean smooth = (flag & FLAG_SMOOTH) != 0; |
||||
int flagU = ((Number) nurb.getFieldValue("flagu")).intValue(); |
||||
int flagV = ((Number) nurb.getFieldValue("flagv")).intValue(); |
||||
int orderU = ((Number) nurb.getFieldValue("orderu")).intValue(); |
||||
int orderV = ((Number) nurb.getFieldValue("orderv")).intValue(); |
||||
|
||||
// loading control points and their weights
|
||||
int pntsU = ((Number) nurb.getFieldValue("pntsu")).intValue(); |
||||
int pntsV = ((Number) nurb.getFieldValue("pntsv")).intValue(); |
||||
List<Structure> bPoints = ((Pointer) nurb.getFieldValue("bp")).fetchData(); |
||||
List<List<Vector4f>> controlPoints = new ArrayList<List<Vector4f>>(pntsV); |
||||
for (int i = 0; i < pntsV; ++i) { |
||||
List<Vector4f> uControlPoints = new ArrayList<Vector4f>(pntsU); |
||||
for (int j = 0; j < pntsU; ++j) { |
||||
DynamicArray<Float> vec = (DynamicArray<Float>) bPoints.get(j + i * pntsU).getFieldValue("vec"); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(2).floatValue(), -vec.get(1).floatValue(), vec.get(3).floatValue())); |
||||
} else { |
||||
uControlPoints.add(new Vector4f(vec.get(0).floatValue(), vec.get(1).floatValue(), vec.get(2).floatValue(), vec.get(3).floatValue())); |
||||
} |
||||
} |
||||
if ((flagU & 0x01) != 0) { |
||||
for (int k = 0; k < orderU - 1; ++k) { |
||||
uControlPoints.add(uControlPoints.get(k)); |
||||
} |
||||
} |
||||
controlPoints.add(uControlPoints); |
||||
} |
||||
if ((flagV & 0x01) != 0) { |
||||
for (int k = 0; k < orderV - 1; ++k) { |
||||
controlPoints.add(controlPoints.get(k)); |
||||
} |
||||
} |
||||
|
||||
int originalVerticesAmount = vertices.size(); |
||||
int resolu = ((Number) nurb.getFieldValue("resolu")).intValue(); |
||||
if (knots[1] == null) {// creating the NURB curve
|
||||
Curve curve = new Curve(new Spline(controlPoints.get(0), knots[0]), resolu); |
||||
FloatBuffer vertsBuffer = (FloatBuffer) curve.getBuffer(Type.Position).getData(); |
||||
beziers.add(new BezierLine(BufferUtils.getVector3Array(vertsBuffer), materialIndex, smooth, false)); |
||||
} else {// creating the NURB surface
|
||||
int resolv = ((Number) nurb.getFieldValue("resolv")).intValue(); |
||||
int uSegments = resolu * controlPoints.get(0).size() - 1; |
||||
int vSegments = resolv * controlPoints.size() - 1; |
||||
Surface nurbSurface = Surface.createNurbsSurface(controlPoints, knots, uSegments, vSegments, orderU, orderV, smooth); |
||||
|
||||
FloatBuffer vertsBuffer = (FloatBuffer) nurbSurface.getBuffer(Type.Position).getData(); |
||||
vertices.addAll(Arrays.asList(BufferUtils.getVector3Array(vertsBuffer))); |
||||
FloatBuffer normalsBuffer = (FloatBuffer) nurbSurface.getBuffer(Type.Normal).getData(); |
||||
normals.addAll(Arrays.asList(BufferUtils.getVector3Array(normalsBuffer))); |
||||
|
||||
IndexBuffer indexBuffer = nurbSurface.getIndexBuffer(); |
||||
for (int i = 0; i < indexBuffer.size(); i += 3) { |
||||
int index1 = indexBuffer.get(i) + originalVerticesAmount; |
||||
int index2 = indexBuffer.get(i + 1) + originalVerticesAmount; |
||||
int index3 = indexBuffer.get(i + 2) + originalVerticesAmount; |
||||
faces.add(new Face(new Integer[] { index1, index2, index3 }, smooth, materialIndex, null, null, this)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method loads the bevel object that should be applied to curve. It can either be another curve or a generated one |
||||
* based on the bevel generating parameters in blender. |
||||
* @param curveStructure |
||||
* the structure with the curve's data (the curve being loaded, NOT the bevel curve) |
||||
* @return the curve's bevel object |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading occur |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private CurvesTemporalMesh loadBevelObject(Structure curveStructure) throws BlenderFileException { |
||||
CurvesTemporalMesh bevelObject = null; |
||||
Pointer pBevelObject = (Pointer) curveStructure.getFieldValue("bevobj"); |
||||
boolean cyclic = false; |
||||
if (pBevelObject.isNotNull()) { |
||||
Structure bevelObjectStructure = pBevelObject.fetchData().get(0); |
||||
DynamicArray<Number> scaleArray = (DynamicArray<Number>) bevelObjectStructure.getFieldValue("size"); |
||||
Vector3f scale = blenderContext.getBlenderKey().isFixUpAxis() ? new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue()) : new Vector3f(scaleArray.get(0).floatValue(), scaleArray.get(2).floatValue(), scaleArray.get(1).floatValue()); |
||||
Pointer pBevelStructure = (Pointer) bevelObjectStructure.getFieldValue("data"); |
||||
Structure bevelStructure = pBevelStructure.fetchData().get(0); |
||||
bevelObject = new CurvesTemporalMesh(bevelStructure, scale, false, blenderContext); |
||||
|
||||
// transforming the bezier lines from plane XZ to plane YZ
|
||||
for (BezierLine bl : bevelObject.beziers) { |
||||
for (Vector3f v : bl.vertices) { |
||||
// casting the bezier curve orthogonally on the plane XZ (making Y = 0) and then moving the plane XZ to ZY in a way that:
|
||||
// -Z => +Y and +X => +Z and +Y => +X (but because casting would make Y = 0, then we simply set X = 0)
|
||||
v.y = -v.z; |
||||
v.z = v.x; |
||||
v.x = 0; |
||||
} |
||||
|
||||
// bevel curves should not have repeated the first vertex at the end when they are cyclic (this is handled differently)
|
||||
if (bl.isCyclic()) { |
||||
bl.removeLastVertex(); |
||||
} |
||||
} |
||||
} else { |
||||
fillCaps = false;// this option is inactive in blender when there is no bevel object applied
|
||||
int bevResol = ((Number) curveStructure.getFieldValue("bevresol")).intValue(); |
||||
float extrude = ((Number) curveStructure.getFieldValue("ext1")).floatValue(); |
||||
float bevelDepth = ((Number) curveStructure.getFieldValue("ext2")).floatValue(); |
||||
float offset = ((Number) curveStructure.getFieldValue("offset", 0)).floatValue(); |
||||
if (offset != 0) { |
||||
// TODO: add support for offset parameter
|
||||
LOGGER.warning("Offset parameter not yet supported."); |
||||
} |
||||
Curve bevelCurve = null; |
||||
if (bevelDepth > 0.0f) { |
||||
float handlerLength = bevelDepth / 2.0f; |
||||
cyclic = !isFront && !isBack; |
||||
List<Vector3f> conrtolPoints = new ArrayList<Vector3f>(); |
||||
|
||||
// blenders from 2.49 to 2.52 did not pay attention to fron and back faces
|
||||
// so in order to draw the scene exactly as it is in different blender versions the blender version is checked here
|
||||
// when neither fron and back face is selected all version behave the same and draw full bevel around the curve
|
||||
if (cyclic || blenderContext.getBlenderVersion() < 253) { |
||||
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, 0)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, -handlerLength)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, -extrude - handlerLength, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude + handlerLength, -bevelDepth)); |
||||
|
||||
if (extrude > 0) { |
||||
conrtolPoints.add(new Vector3f(0, extrude - handlerLength, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, extrude, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, extrude + handlerLength, -bevelDepth)); |
||||
} |
||||
|
||||
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, -handlerLength)); |
||||
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, 0)); |
||||
|
||||
if (cyclic) { |
||||
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, handlerLength)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, extrude + handlerLength, bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, extrude, bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, extrude - handlerLength, bevelDepth)); |
||||
|
||||
if (extrude > 0) { |
||||
conrtolPoints.add(new Vector3f(0, -extrude + handlerLength, bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude, bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude - handlerLength, bevelDepth)); |
||||
} |
||||
|
||||
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, handlerLength)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, 0)); |
||||
} |
||||
} else { |
||||
if (extrude > 0) { |
||||
if (isBack) { |
||||
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, 0)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude - bevelDepth, -handlerLength)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, -extrude - handlerLength, -bevelDepth)); |
||||
} |
||||
|
||||
conrtolPoints.add(new Vector3f(0, -extrude, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, -extrude + handlerLength, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, extrude - handlerLength, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, extrude, -bevelDepth)); |
||||
|
||||
if (isFront) { |
||||
conrtolPoints.add(new Vector3f(0, extrude + handlerLength, -bevelDepth)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, -handlerLength)); |
||||
conrtolPoints.add(new Vector3f(0, extrude + bevelDepth, 0)); |
||||
} |
||||
} else { |
||||
if (isFront && isBack) { |
||||
conrtolPoints.add(new Vector3f(0, -bevelDepth, 0)); |
||||
conrtolPoints.add(new Vector3f(0, -bevelDepth, -handlerLength)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, -handlerLength, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, handlerLength, -bevelDepth)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, bevelDepth, -handlerLength)); |
||||
conrtolPoints.add(new Vector3f(0, bevelDepth, 0)); |
||||
} else { |
||||
if (isBack) { |
||||
conrtolPoints.add(new Vector3f(0, -bevelDepth, 0)); |
||||
conrtolPoints.add(new Vector3f(0, -bevelDepth, -handlerLength)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, -handlerLength, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth)); |
||||
} else { |
||||
conrtolPoints.add(new Vector3f(0, 0, -bevelDepth)); |
||||
conrtolPoints.add(new Vector3f(0, handlerLength, -bevelDepth)); |
||||
|
||||
conrtolPoints.add(new Vector3f(0, bevelDepth, -handlerLength)); |
||||
conrtolPoints.add(new Vector3f(0, bevelDepth, 0)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
bevelCurve = new Curve(new Spline(SplineType.Bezier, conrtolPoints, 0, false), bevResol); |
||||
} else if (extrude > 0.0f) { |
||||
Spline bevelSpline = new Spline(SplineType.Linear, new Vector3f[] { new Vector3f(0, extrude, 0), new Vector3f(0, -extrude, 0) }, 1, false); |
||||
bevelCurve = new Curve(bevelSpline, bevResol); |
||||
} |
||||
if (bevelCurve != null) { |
||||
bevelObject = new CurvesTemporalMesh(blenderContext); |
||||
FloatBuffer vertsBuffer = (FloatBuffer) bevelCurve.getBuffer(Type.Position).getData(); |
||||
Vector3f[] verts = BufferUtils.getVector3Array(vertsBuffer); |
||||
if (cyclic) {// get rid of the last vertex which is identical to the first one
|
||||
verts = Arrays.copyOf(verts, verts.length - 1); |
||||
} |
||||
bevelObject.beziers.add(new BezierLine(verts, 0, false, cyclic)); |
||||
} |
||||
} |
||||
return bevelObject; |
||||
} |
||||
|
||||
private List<BezierLine> getScaledBeziers() { |
||||
if (scale.equals(Vector3f.UNIT_XYZ)) { |
||||
return beziers; |
||||
} |
||||
List<BezierLine> result = new ArrayList<BezierLine>(); |
||||
for (BezierLine bezierLine : beziers) { |
||||
result.add(bezierLine.scale(scale)); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method applies bevel and taper objects to the curve. |
||||
* @param curve |
||||
* the curve we apply the objects to |
||||
* @param bevelObject |
||||
* the bevel object |
||||
* @param taperObject |
||||
* the taper object |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of geometries representing the beveled and/or tapered curve |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading occur |
||||
*/ |
||||
private CurvesTemporalMesh applyBevelAndTaper(CurvesTemporalMesh curve, CurvesTemporalMesh bevelObject, CurvesTemporalMesh taperObject, BlenderContext blenderContext) throws BlenderFileException { |
||||
List<BezierLine> bevelBezierLines = bevelObject.getScaledBeziers(); |
||||
List<BezierLine> curveLines = curve.beziers; |
||||
if (bevelBezierLines.size() == 0 || curveLines.size() == 0) { |
||||
return null; |
||||
} |
||||
|
||||
CurvesTemporalMesh result = new CurvesTemporalMesh(blenderContext); |
||||
for (BezierLine curveLine : curveLines) { |
||||
Vector3f[] curveLineVertices = curveLine.getVertices(bevelStart, bevelEnd); |
||||
|
||||
for (BezierLine bevelBezierLine : bevelBezierLines) { |
||||
CurvesTemporalMesh partResult = new CurvesTemporalMesh(blenderContext); |
||||
|
||||
Vector3f[] bevelLineVertices = bevelBezierLine.getVertices(); |
||||
List<Vector3f[]> bevels = new ArrayList<Vector3f[]>(); |
||||
|
||||
Vector3f[] bevelPoints = curvesHelper.transformToFirstLineOfBevelPoints(bevelLineVertices, curveLineVertices[0], curveLineVertices[1]); |
||||
bevels.add(bevelPoints); |
||||
for (int i = 1; i < curveLineVertices.length - 1; ++i) { |
||||
bevelPoints = curvesHelper.transformBevel(bevelPoints, curveLineVertices[i - 1], curveLineVertices[i], curveLineVertices[i + 1]); |
||||
bevels.add(bevelPoints); |
||||
} |
||||
bevelPoints = curvesHelper.transformBevel(bevelPoints, curveLineVertices[curveLineVertices.length - 2], curveLineVertices[curveLineVertices.length - 1], null); |
||||
bevels.add(bevelPoints); |
||||
|
||||
Vector3f subtractResult = new Vector3f(); |
||||
if (bevels.size() > 2) { |
||||
// changing the first and last bevel so that they are parallel to their neighbours (blender works this way)
|
||||
// notice this implicates that the distances of every corresponding point in the two bevels must be identical and
|
||||
// equal to the distance between the points on curve that define the bevel position
|
||||
// so instead doing complicated rotations on each point we will simply properly translate each of them
|
||||
int[][] pointIndexes = new int[][] { { 0, 1 }, { curveLineVertices.length - 1, curveLineVertices.length - 2 } }; |
||||
for (int[] indexes : pointIndexes) { |
||||
float distance = curveLineVertices[indexes[1]].subtract(curveLineVertices[indexes[0]], subtractResult).length(); |
||||
Vector3f[] bevel = bevels.get(indexes[0]); |
||||
Vector3f[] nextBevel = bevels.get(indexes[1]); |
||||
for (int i = 0; i < bevel.length; ++i) { |
||||
float d = bevel[i].subtract(nextBevel[i], subtractResult).length(); |
||||
subtractResult.normalizeLocal().multLocal(distance - d); |
||||
bevel[i].addLocal(subtractResult); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (taperObject != null) { |
||||
float curveLength = curveLine.getLength(), lengthAlongCurve = bevelStart; |
||||
for (int i = 0; i < curveLineVertices.length; ++i) { |
||||
if (i > 0) { |
||||
lengthAlongCurve += curveLineVertices[i].subtract(curveLineVertices[i - 1], subtractResult).length(); |
||||
} |
||||
float taperScale = -taperObject.getValueAlongCurve(lengthAlongCurve / curveLength).z * taperObject.scale.z; |
||||
if (taperScale != 1) { |
||||
this.applyScale(bevels.get(i), curveLineVertices[i], taperScale); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// adding vertices to the part result
|
||||
for (Vector3f[] bevel : bevels) { |
||||
for (Vector3f d : bevel) { |
||||
partResult.getVertices().add(d); |
||||
} |
||||
} |
||||
|
||||
// preparing faces for the part result (each face is a quad)
|
||||
int bevelVertCount = bevelPoints.length; |
||||
for (int i = 0; i < bevels.size() - 1; ++i) { |
||||
for (int j = 0; j < bevelVertCount - 1; ++j) { |
||||
Integer[] indexes = new Integer[] { i * bevelVertCount + j + 1, (i + 1) * bevelVertCount + j + 1, (i + 1) * bevelVertCount + j, i * bevelVertCount + j }; |
||||
partResult.getFaces().add(new Face(indexes, curveLine.isSmooth(), curveLine.getMaterialNumber(), null, null, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[0], indexes[1], 0, true, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[1], indexes[2], 0, true, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[2], indexes[3], 0, true, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[3], indexes[0], 0, true, partResult)); |
||||
} |
||||
if (bevelBezierLine.isCyclic()) { |
||||
int j = bevelVertCount - 1; |
||||
Integer[] indexes = new Integer[] { i * bevelVertCount, (i + 1) * bevelVertCount, (i + 1) * bevelVertCount + j, i * bevelVertCount + j }; |
||||
partResult.getFaces().add(new Face(indexes, curveLine.isSmooth(), curveLine.getMaterialNumber(), null, null, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[0], indexes[1], 0, true, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[1], indexes[2], 0, true, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[2], indexes[3], 0, true, partResult)); |
||||
partResult.getEdges().add(new Edge(indexes[3], indexes[0], 0, true, partResult)); |
||||
} |
||||
} |
||||
|
||||
partResult.generateNormals(); |
||||
|
||||
if (fillCaps) {// caps in blender behave as if they weren't affected by the smooth factor
|
||||
// START CAP
|
||||
Vector3f[] cap = bevels.get(0); |
||||
List<Integer> capIndexes = new ArrayList<Integer>(cap.length); |
||||
Vector3f capNormal = curveLineVertices[0].subtract(curveLineVertices[1]).normalizeLocal(); |
||||
for (int i = 0; i < cap.length; ++i) { |
||||
capIndexes.add(partResult.getVertices().size()); |
||||
partResult.getVertices().add(cap[i]); |
||||
partResult.getNormals().add(capNormal); |
||||
} |
||||
Collections.reverse(capIndexes);// the indexes ned to be reversed for the face to have fron face outside the beveled line
|
||||
partResult.getFaces().add(new Face(capIndexes.toArray(new Integer[capIndexes.size()]), false, curveLine.getMaterialNumber(), null, null, partResult)); |
||||
for (int i = 1; i < capIndexes.size(); ++i) { |
||||
partResult.getEdges().add(new Edge(capIndexes.get(i - 1), capIndexes.get(i), 0, true, partResult)); |
||||
} |
||||
|
||||
// END CAP
|
||||
cap = bevels.get(bevels.size() - 1); |
||||
capIndexes.clear(); |
||||
capNormal = curveLineVertices[curveLineVertices.length - 1].subtract(curveLineVertices[curveLineVertices.length - 2]).normalizeLocal(); |
||||
for (int i = 0; i < cap.length; ++i) { |
||||
capIndexes.add(partResult.getVertices().size()); |
||||
partResult.getVertices().add(cap[i]); |
||||
partResult.getNormals().add(capNormal); |
||||
} |
||||
partResult.getFaces().add(new Face(capIndexes.toArray(new Integer[capIndexes.size()]), false, curveLine.getMaterialNumber(), null, null, partResult)); |
||||
for (int i = 1; i < capIndexes.size(); ++i) { |
||||
partResult.getEdges().add(new Edge(capIndexes.get(i - 1), capIndexes.get(i), 0, true, partResult)); |
||||
} |
||||
} |
||||
|
||||
result.append(partResult); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method generates normals for the curve. If any normals were already stored they are discarded. |
||||
*/ |
||||
private void generateNormals() { |
||||
Map<Integer, Vector3f> normalMap = new TreeMap<Integer, Vector3f>(); |
||||
for (Face face : faces) { |
||||
// the first 3 verts are enough here (all faces are triangles except for the caps, but those are fully flat anyway)
|
||||
int index1 = face.getIndexes().get(0); |
||||
int index2 = face.getIndexes().get(1); |
||||
int index3 = face.getIndexes().get(2); |
||||
|
||||
Vector3f n = FastMath.computeNormal(vertices.get(index1), vertices.get(index2), vertices.get(index3)); |
||||
for (int index : face.getIndexes()) { |
||||
Vector3f normal = normalMap.get(index); |
||||
if (normal == null) { |
||||
normalMap.put(index, n.clone()); |
||||
} else { |
||||
normal.addLocal(n).normalizeLocal(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
normals.clear(); |
||||
Collections.addAll(normals, new Vector3f[normalMap.size()]); |
||||
for (Entry<Integer, Vector3f> entry : normalMap.entrySet()) { |
||||
normals.set(entry.getKey(), entry.getValue()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* the method applies scale for the given bevel points. The points table is |
||||
* being modified so expect your result there. |
||||
* |
||||
* @param points |
||||
* the bevel points |
||||
* @param centerPoint |
||||
* the center point of the bevel |
||||
* @param scale |
||||
* the scale to be applied |
||||
*/ |
||||
private void applyScale(Vector3f[] points, Vector3f centerPoint, float scale) { |
||||
Vector3f taperScaleVector = new Vector3f(); |
||||
for (Vector3f p : points) { |
||||
taperScaleVector.set(centerPoint).subtractLocal(p).multLocal(1 - scale); |
||||
p.addLocal(taperScaleVector); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A helper class that represents a single bezier line. It consists of Edge's and allows to |
||||
* get a subline of a length of the line. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static class BezierLine { |
||||
/** The edges of the bezier line. */ |
||||
private Vector3f[] vertices; |
||||
/** The material number of the line. */ |
||||
private int materialNumber; |
||||
/** Indicates if the line is smooth of flat. */ |
||||
private boolean smooth; |
||||
/** The length of the line. */ |
||||
private float length; |
||||
/** Indicates if the current line is cyclic or not. */ |
||||
private boolean cyclic; |
||||
|
||||
public BezierLine(Vector3f[] vertices, int materialNumber, boolean smooth, boolean cyclik) { |
||||
this.vertices = vertices; |
||||
this.materialNumber = materialNumber; |
||||
this.smooth = smooth; |
||||
cyclic = cyclik; |
||||
this.recomputeLength(); |
||||
} |
||||
|
||||
public BezierLine scale(Vector3f scale) { |
||||
BezierLine result = new BezierLine(vertices, materialNumber, smooth, cyclic); |
||||
result.vertices = new Vector3f[vertices.length]; |
||||
for (int i = 0; i < vertices.length; ++i) { |
||||
result.vertices[i] = vertices[i].mult(scale); |
||||
} |
||||
result.recomputeLength(); |
||||
return result; |
||||
} |
||||
|
||||
public void removeLastVertex() { |
||||
Vector3f[] newVertices = new Vector3f[vertices.length - 1]; |
||||
for (int i = 0; i < vertices.length - 1; ++i) { |
||||
newVertices[i] = vertices[i]; |
||||
} |
||||
vertices = newVertices; |
||||
this.recomputeLength(); |
||||
} |
||||
|
||||
private void recomputeLength() { |
||||
length = 0; |
||||
for (int i = 1; i < vertices.length; ++i) { |
||||
length += vertices[i - 1].distance(vertices[i]); |
||||
} |
||||
if (cyclic) { |
||||
// if the first vertex is repeated at the end the distance will be = 0 so it won't affect the result, and if it is not repeated
|
||||
// then it is necessary to add the length between the last and the first vertex
|
||||
length += vertices[vertices.length - 1].distance(vertices[0]); |
||||
} |
||||
} |
||||
|
||||
public Vector3f[] getVertices() { |
||||
return this.getVertices(0, 1); |
||||
} |
||||
|
||||
public Vector3f[] getVertices(float startSlice, float endSlice) { |
||||
if (startSlice == 0 && endSlice == 1) { |
||||
return vertices; |
||||
} |
||||
List<Vector3f> result = new ArrayList<Vector3f>(); |
||||
float length = this.getLength(), temp = 0; |
||||
float startSliceLength = length * startSlice; |
||||
float endSliceLength = length * endSlice; |
||||
int index = 1; |
||||
|
||||
if (startSlice > 0) { |
||||
while (temp < startSliceLength) { |
||||
Vector3f v1 = vertices[index - 1]; |
||||
Vector3f v2 = vertices[index++]; |
||||
float edgeLength = v1.distance(v2); |
||||
temp += edgeLength; |
||||
if (temp == startSliceLength) { |
||||
result.add(v2); |
||||
} else if (temp > startSliceLength) { |
||||
result.add(v1.subtract(v2).normalizeLocal().multLocal(temp - startSliceLength).addLocal(v2)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (endSlice < 1) { |
||||
if (index == vertices.length) { |
||||
Vector3f v1 = vertices[vertices.length - 2]; |
||||
Vector3f v2 = vertices[vertices.length - 1]; |
||||
result.add(v1.subtract(v2).normalizeLocal().multLocal(length - endSliceLength).addLocal(v2)); |
||||
} else { |
||||
for (int i = index; i < vertices.length && temp < endSliceLength; ++i) { |
||||
Vector3f v1 = vertices[index - 1]; |
||||
Vector3f v2 = vertices[index++]; |
||||
temp += v1.distance(v2); |
||||
if (temp == endSliceLength) { |
||||
result.add(v2); |
||||
} else if (temp > endSliceLength) { |
||||
result.add(v1.subtract(v2).normalizeLocal().multLocal(temp - startSliceLength).addLocal(v2)); |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
result.addAll(Arrays.asList(Arrays.copyOfRange(vertices, index, vertices.length))); |
||||
} |
||||
|
||||
return result.toArray(new Vector3f[result.size()]); |
||||
} |
||||
|
||||
/** |
||||
* The method computes the value of a point at the certain relational distance from its beginning. |
||||
* @param alongRatio |
||||
* the relative distance along the curve; should be a value between 0 and 1 inclusive; |
||||
* if the value exceeds the boundaries it is truncated to them |
||||
* @return computed value along the curve |
||||
*/ |
||||
public Vector3f getValueAlongCurve(float alongRatio) { |
||||
alongRatio = FastMath.clamp(alongRatio, 0, 1); |
||||
Vector3f result = new Vector3f(); |
||||
float probeLength = this.getLength() * alongRatio; |
||||
float length = 0; |
||||
for (int i = 1; i < vertices.length; ++i) { |
||||
float edgeLength = vertices[i].distance(vertices[i - 1]); |
||||
if (length + edgeLength > probeLength) { |
||||
float ratioAlongEdge = (probeLength - length) / edgeLength; |
||||
return FastMath.interpolateLinear(ratioAlongEdge, vertices[i - 1], vertices[i]); |
||||
} else if (length + edgeLength == probeLength) { |
||||
return vertices[i]; |
||||
} |
||||
length += edgeLength; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return the material number of this bezier line |
||||
*/ |
||||
public int getMaterialNumber() { |
||||
return materialNumber; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the line is smooth of flat |
||||
*/ |
||||
public boolean isSmooth() { |
||||
return smooth; |
||||
} |
||||
|
||||
/** |
||||
* @return the length of this bezier line |
||||
*/ |
||||
public float getLength() { |
||||
return length; |
||||
} |
||||
|
||||
/** |
||||
* @return indicates if the current line is cyclic or not |
||||
*/ |
||||
public boolean isCyclic() { |
||||
return cyclic; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,77 @@ |
||||
/* |
||||
* Copyright (c) 2009-2019 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
/** |
||||
* This exception is thrown when blend file data is somehow invalid. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class BlenderFileException extends Exception { |
||||
|
||||
private static final long serialVersionUID = 7573482836437866767L; |
||||
|
||||
/** |
||||
* Constructor. Creates an exception with no description. |
||||
*/ |
||||
public BlenderFileException() { |
||||
// this constructor has no message
|
||||
} |
||||
|
||||
/** |
||||
* Constructor. Creates an exception containing the given message. |
||||
* @param message |
||||
* the message describing the problem that occurred |
||||
*/ |
||||
public BlenderFileException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Creates an exception that is based upon other thrown object. It contains the whole stacktrace then. |
||||
* @param throwable |
||||
* an exception/error that occurred |
||||
*/ |
||||
public BlenderFileException(Throwable throwable) { |
||||
super(throwable); |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Creates an exception with both a message and stacktrace. |
||||
* @param message |
||||
* the message describing the problem that occurred |
||||
* @param throwable |
||||
* an exception/error that occurred |
||||
*/ |
||||
public BlenderFileException(String message, Throwable throwable) { |
||||
super(message, throwable); |
||||
} |
||||
} |
@ -0,0 +1,371 @@ |
||||
/* |
||||
* Copyright (c) 2009-2019 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
import java.io.BufferedInputStream; |
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.util.logging.Logger; |
||||
import java.util.zip.GZIPInputStream; |
||||
|
||||
/** |
||||
* An input stream with random access to data. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class BlenderInputStream extends InputStream { |
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(BlenderInputStream.class.getName()); |
||||
/** The default size of the blender buffer. */ |
||||
private static final int DEFAULT_BUFFER_SIZE = 1048576; // 1MB
|
||||
/** |
||||
* Size of a pointer; all pointers in the file are stored in this format. '_' means 4 bytes and '-' means 8 bytes. |
||||
*/ |
||||
private int pointerSize; |
||||
/** |
||||
* Type of byte ordering used; 'v' means little endian and 'V' means big endian. |
||||
*/ |
||||
private char endianess; |
||||
/** Version of Blender the file was created in; '248' means version 2.48. */ |
||||
private String versionNumber; |
||||
/** The buffer we store the read data to. */ |
||||
protected byte[] cachedBuffer; |
||||
/** The total size of the stored data. */ |
||||
protected int size; |
||||
/** The current position of the read cursor. */ |
||||
protected int position; |
||||
|
||||
/** |
||||
* Constructor. The input stream is stored and used to read data. |
||||
* @param inputStream |
||||
* the stream we read data from |
||||
* @throws BlenderFileException |
||||
* this exception is thrown if the file header has some invalid data |
||||
*/ |
||||
public BlenderInputStream(InputStream inputStream) throws BlenderFileException { |
||||
// the size value will canche while reading the file; the available() method cannot be counted on
|
||||
try { |
||||
size = inputStream.available(); |
||||
} catch (IOException e) { |
||||
size = 0; |
||||
} |
||||
if (size <= 0) { |
||||
size = BlenderInputStream.DEFAULT_BUFFER_SIZE; |
||||
} |
||||
|
||||
// buffered input stream is used here for much faster file reading
|
||||
BufferedInputStream bufferedInputStream; |
||||
if (inputStream instanceof BufferedInputStream) { |
||||
bufferedInputStream = (BufferedInputStream) inputStream; |
||||
} else { |
||||
bufferedInputStream = new BufferedInputStream(inputStream); |
||||
} |
||||
|
||||
try { |
||||
this.readStreamToCache(bufferedInputStream); |
||||
} catch (IOException e) { |
||||
throw new BlenderFileException("Problems occurred while caching the file!", e); |
||||
} finally { |
||||
try { |
||||
inputStream.close(); |
||||
} catch (IOException e) { |
||||
LOGGER.warning("Unable to close stream with blender file."); |
||||
} |
||||
} |
||||
|
||||
try { |
||||
this.readFileHeader(); |
||||
} catch (BlenderFileException e) {// the file might be packed, don't panic, try one more time ;)
|
||||
this.decompressFile(); |
||||
position = 0; |
||||
this.readFileHeader(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method reads the whole stream into a buffer. |
||||
* @param inputStream |
||||
* the stream to read the file data from |
||||
* @throws IOException |
||||
* an exception is thrown when data read from the stream is invalid or there are problems with i/o |
||||
* operations |
||||
*/ |
||||
private void readStreamToCache(InputStream inputStream) throws IOException { |
||||
int data = inputStream.read(); |
||||
cachedBuffer = new byte[size]; |
||||
size = 0;// this will count the actual size
|
||||
while (data != -1) { |
||||
if (size >= cachedBuffer.length) {// widen the cached array
|
||||
byte[] newBuffer = new byte[cachedBuffer.length + (cachedBuffer.length >> 1)]; |
||||
System.arraycopy(cachedBuffer, 0, newBuffer, 0, cachedBuffer.length); |
||||
cachedBuffer = newBuffer; |
||||
} |
||||
cachedBuffer[size++] = (byte) data; |
||||
data = inputStream.read(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method is used when the blender file is gzipped. It decompresses the data and stores it back into the |
||||
* cachedBuffer field. |
||||
*/ |
||||
private void decompressFile() { |
||||
GZIPInputStream gis = null; |
||||
try { |
||||
gis = new GZIPInputStream(new ByteArrayInputStream(cachedBuffer)); |
||||
this.readStreamToCache(gis); |
||||
} catch (IOException e) { |
||||
throw new IllegalStateException("IO errors occurred where they should NOT! " + "The data is already buffered at this point!", e); |
||||
} finally { |
||||
try { |
||||
if (gis != null) { |
||||
gis.close(); |
||||
} |
||||
} catch (IOException e) { |
||||
LOGGER.warning(e.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method loads the header from the given stream during instance creation. |
||||
* @param inputStream |
||||
* the stream we read the header from |
||||
* @throws BlenderFileException |
||||
* this exception is thrown if the file header has some invalid data |
||||
*/ |
||||
private void readFileHeader() throws BlenderFileException { |
||||
byte[] identifier = new byte[7]; |
||||
int bytesRead = this.readBytes(identifier); |
||||
if (bytesRead != 7) { |
||||
throw new BlenderFileException("Error reading header identifier. Only " + bytesRead + " bytes read and there should be 7!"); |
||||
} |
||||
String strIdentifier = new String(identifier); |
||||
if (!"BLENDER".equals(strIdentifier)) { |
||||
throw new BlenderFileException("Wrong file identifier: " + strIdentifier + "! Should be 'BLENDER'!"); |
||||
} |
||||
char pointerSizeSign = (char) this.readByte(); |
||||
if (pointerSizeSign == '-') { |
||||
pointerSize = 8; |
||||
} else if (pointerSizeSign == '_') { |
||||
pointerSize = 4; |
||||
} else { |
||||
throw new BlenderFileException("Invalid pointer size character! Should be '_' or '-' and there is: " + pointerSizeSign); |
||||
} |
||||
endianess = (char) this.readByte(); |
||||
if (endianess != 'v' && endianess != 'V') { |
||||
throw new BlenderFileException("Unknown endianess value! 'v' or 'V' expected and found: " + endianess); |
||||
} |
||||
byte[] versionNumber = new byte[3]; |
||||
bytesRead = this.readBytes(versionNumber); |
||||
if (bytesRead != 3) { |
||||
throw new BlenderFileException("Error reading version numberr. Only " + bytesRead + " bytes read and there should be 3!"); |
||||
} |
||||
this.versionNumber = new String(versionNumber); |
||||
} |
||||
|
||||
@Override |
||||
public int read() throws IOException { |
||||
return this.readByte(); |
||||
} |
||||
|
||||
/** |
||||
* This method reads 1 byte from the stream. |
||||
* It works just in the way the read method does. |
||||
* It just not throw an exception because at this moment the whole file |
||||
* is loaded into buffer, so no need for IOException to be thrown. |
||||
* @return a byte from the stream (1 bytes read) |
||||
*/ |
||||
public int readByte() { |
||||
return cachedBuffer[position++] & 0xFF; |
||||
} |
||||
|
||||
/** |
||||
* This method reads a bytes number big enough to fill the table. |
||||
* It does not throw exceptions so it is for internal use only. |
||||
* @param bytes |
||||
* an array to be filled with data |
||||
* @return number of read bytes (a length of array actually) |
||||
*/ |
||||
private int readBytes(byte[] bytes) { |
||||
for (int i = 0; i < bytes.length; ++i) { |
||||
bytes[i] = (byte) this.readByte(); |
||||
} |
||||
return bytes.length; |
||||
} |
||||
|
||||
/** |
||||
* This method reads 2-byte number from the stream. |
||||
* @return a number from the stream (2 bytes read) |
||||
*/ |
||||
public int readShort() { |
||||
int part1 = this.readByte(); |
||||
int part2 = this.readByte(); |
||||
if (endianess == 'v') { |
||||
return (part2 << 8) + part1; |
||||
} else { |
||||
return (part1 << 8) + part2; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method reads 4-byte number from the stream. |
||||
* @return a number from the stream (4 bytes read) |
||||
*/ |
||||
public int readInt() { |
||||
int part1 = this.readByte(); |
||||
int part2 = this.readByte(); |
||||
int part3 = this.readByte(); |
||||
int part4 = this.readByte(); |
||||
if (endianess == 'v') { |
||||
return (part4 << 24) + (part3 << 16) + (part2 << 8) + part1; |
||||
} else { |
||||
return (part1 << 24) + (part2 << 16) + (part3 << 8) + part4; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method reads 4-byte floating point number (float) from the stream. |
||||
* @return a number from the stream (4 bytes read) |
||||
*/ |
||||
public float readFloat() { |
||||
int intValue = this.readInt(); |
||||
return Float.intBitsToFloat(intValue); |
||||
} |
||||
|
||||
/** |
||||
* This method reads 8-byte number from the stream. |
||||
* @return a number from the stream (8 bytes read) |
||||
*/ |
||||
public long readLong() { |
||||
long part1 = this.readInt(); |
||||
long part2 = this.readInt(); |
||||
long result = -1; |
||||
if (endianess == 'v') { |
||||
result = part2 << 32 | part1; |
||||
} else { |
||||
result = part1 << 32 | part2; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method reads 8-byte floating point number (double) from the stream. |
||||
* @return a number from the stream (8 bytes read) |
||||
*/ |
||||
public double readDouble() { |
||||
long longValue = this.readLong(); |
||||
return Double.longBitsToDouble(longValue); |
||||
} |
||||
|
||||
/** |
||||
* This method reads the pointer value. Depending on the pointer size defined in the header, the stream reads either |
||||
* 4 or 8 bytes of data. |
||||
* @return the pointer value |
||||
*/ |
||||
public long readPointer() { |
||||
if (pointerSize == 4) { |
||||
return this.readInt(); |
||||
} |
||||
return this.readLong(); |
||||
} |
||||
|
||||
/** |
||||
* This method reads the string. It assumes the string is terminated with zero in the stream. |
||||
* @return the string read from the stream |
||||
*/ |
||||
public String readString() { |
||||
StringBuilder stringBuilder = new StringBuilder(); |
||||
int data = this.readByte(); |
||||
while (data != 0) { |
||||
stringBuilder.append((char) data); |
||||
data = this.readByte(); |
||||
} |
||||
return stringBuilder.toString(); |
||||
} |
||||
|
||||
/** |
||||
* This method sets the current position of the read cursor. |
||||
* @param position |
||||
* the position of the read cursor |
||||
*/ |
||||
public void setPosition(int position) { |
||||
this.position = position; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the position of the read cursor. |
||||
* @return the position of the read cursor |
||||
*/ |
||||
public int getPosition() { |
||||
return position; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the blender version number where the file was created. |
||||
* @return blender version number |
||||
*/ |
||||
public String getVersionNumber() { |
||||
return versionNumber; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the size of the pointer. |
||||
* @return the size of the pointer |
||||
*/ |
||||
public int getPointerSize() { |
||||
return pointerSize; |
||||
} |
||||
|
||||
/** |
||||
* This method aligns cursor position forward to a given amount of bytes. |
||||
* @param bytesAmount |
||||
* the byte amount to which we aligh the cursor |
||||
*/ |
||||
public void alignPosition(int bytesAmount) { |
||||
if (bytesAmount <= 0) { |
||||
throw new IllegalArgumentException("Alignment byte number shoulf be positivbe!"); |
||||
} |
||||
long move = position % bytesAmount; |
||||
if (move > 0) { |
||||
position += bytesAmount - move; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void close() throws IOException { |
||||
// this method is unimplemented because some loaders (ie. TGALoader) tend close the stream given from the outside
|
||||
// because the images can be stored directly in the blender file then this stream is properly positioned and given to the loader
|
||||
// to read the image file, that is why we do not want it to be closed before the reading is done
|
||||
// and anyway this stream is only a cached buffer, so it does not hold any open connection to anything
|
||||
} |
||||
} |
@ -0,0 +1,203 @@ |
||||
/* |
||||
* Copyright (c) 2009-2018 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* The data block containing the description of the file. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class DnaBlockData { |
||||
|
||||
private static final int SDNA_ID = 'S' << 24 | 'D' << 16 | 'N' << 8 | 'A'; // SDNA
|
||||
private static final int NAME_ID = 'N' << 24 | 'A' << 16 | 'M' << 8 | 'E'; // NAME
|
||||
private static final int TYPE_ID = 'T' << 24 | 'Y' << 16 | 'P' << 8 | 'E'; // TYPE
|
||||
private static final int TLEN_ID = 'T' << 24 | 'L' << 16 | 'E' << 8 | 'N'; // TLEN
|
||||
private static final int STRC_ID = 'S' << 24 | 'T' << 16 | 'R' << 8 | 'C'; // STRC
|
||||
/** Structures available inside the file. */ |
||||
private final Structure[] structures; |
||||
/** A map that helps finding a structure by type. */ |
||||
private final Map<String, Structure> structuresMap; |
||||
|
||||
/** |
||||
* Constructor. Loads the block from the given stream during instance creation. |
||||
* @param inputStream |
||||
* the stream we read the block from |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is throw if the blend file is invalid or somehow corrupted |
||||
*/ |
||||
public DnaBlockData(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException { |
||||
int identifier; |
||||
|
||||
// reading 'SDNA' identifier
|
||||
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte(); |
||||
|
||||
if (identifier != SDNA_ID) { |
||||
throw new BlenderFileException("Invalid identifier! '" + this.toString(SDNA_ID) + "' expected and found: " + this.toString(identifier)); |
||||
} |
||||
|
||||
// reading names
|
||||
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte(); |
||||
if (identifier != NAME_ID) { |
||||
throw new BlenderFileException("Invalid identifier! '" + this.toString(NAME_ID) + "' expected and found: " + this.toString(identifier)); |
||||
} |
||||
int amount = inputStream.readInt(); |
||||
if (amount <= 0) { |
||||
throw new BlenderFileException("The names amount number should be positive!"); |
||||
} |
||||
String[] names = new String[amount]; |
||||
for (int i = 0; i < amount; ++i) { |
||||
names[i] = inputStream.readString(); |
||||
} |
||||
|
||||
// reading types
|
||||
inputStream.alignPosition(4); |
||||
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte(); |
||||
if (identifier != TYPE_ID) { |
||||
throw new BlenderFileException("Invalid identifier! '" + this.toString(TYPE_ID) + "' expected and found: " + this.toString(identifier)); |
||||
} |
||||
amount = inputStream.readInt(); |
||||
if (amount <= 0) { |
||||
throw new BlenderFileException("The types amount number should be positive!"); |
||||
} |
||||
String[] types = new String[amount]; |
||||
for (int i = 0; i < amount; ++i) { |
||||
types[i] = inputStream.readString(); |
||||
} |
||||
|
||||
// reading lengths
|
||||
inputStream.alignPosition(4); |
||||
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte(); |
||||
if (identifier != TLEN_ID) { |
||||
throw new BlenderFileException("Invalid identifier! '" + this.toString(TLEN_ID) + "' expected and found: " + this.toString(identifier)); |
||||
} |
||||
int[] lengths = new int[amount];// theamount is the same as int types
|
||||
for (int i = 0; i < amount; ++i) { |
||||
lengths[i] = inputStream.readShort(); |
||||
} |
||||
|
||||
// reading structures
|
||||
inputStream.alignPosition(4); |
||||
identifier = inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte(); |
||||
if (identifier != STRC_ID) { |
||||
throw new BlenderFileException("Invalid identifier! '" + this.toString(STRC_ID) + "' expected and found: " + this.toString(identifier)); |
||||
} |
||||
amount = inputStream.readInt(); |
||||
if (amount <= 0) { |
||||
throw new BlenderFileException("The structures amount number should be positive!"); |
||||
} |
||||
structures = new Structure[amount]; |
||||
structuresMap = new HashMap<String, Structure>(amount); |
||||
for (int i = 0; i < amount; ++i) { |
||||
structures[i] = new Structure(inputStream, names, types, blenderContext); |
||||
if (structuresMap.containsKey(structures[i].getType())) { |
||||
throw new BlenderFileException("Blend file seems to be corrupted! The type " + structures[i].getType() + " is defined twice!"); |
||||
} |
||||
structuresMap.put(structures[i].getType(), structures[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the amount of the structures. |
||||
* @return the amount of the structures |
||||
*/ |
||||
public int getStructuresCount() { |
||||
return structures.length; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the structure of the given index. |
||||
* @param index |
||||
* the index of the structure |
||||
* @return the structure of the given index |
||||
*/ |
||||
public Structure getStructure(int index) { |
||||
try { |
||||
return (Structure) structures[index].clone(); |
||||
} catch (CloneNotSupportedException e) { |
||||
throw new IllegalStateException("Structure should be clonable!!!", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns a structure of the given name. If the name does not exists then null is returned. |
||||
* @param name |
||||
* the name of the structure |
||||
* @return the required structure or null if the given name is inapropriate |
||||
*/ |
||||
public Structure getStructure(String name) { |
||||
try { |
||||
return (Structure) structuresMap.get(name).clone(); |
||||
} catch (CloneNotSupportedException e) { |
||||
throw new IllegalStateException(e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method indicates if the structure of the given name exists. |
||||
* @param name |
||||
* the name of the structure |
||||
* @return true if the structure exists and false otherwise |
||||
*/ |
||||
public boolean hasStructure(String name) { |
||||
return structuresMap.containsKey(name); |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given identifier code to string. |
||||
* @param code |
||||
* the code that is to be converted |
||||
* @return the string value of the identifier |
||||
*/ |
||||
private String toString(int code) { |
||||
char c1 = (char) ((code & 0xFF000000) >> 24); |
||||
char c2 = (char) ((code & 0xFF0000) >> 16); |
||||
char c3 = (char) ((code & 0xFF00) >> 8); |
||||
char c4 = (char) (code & 0xFF); |
||||
return String.valueOf(c1) + c2 + c3 + c4; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder stringBuilder = new StringBuilder("=============== ").append(SDNA_ID).append('\n'); |
||||
for (Structure structure : structures) { |
||||
stringBuilder.append(structure.toString()).append('\n'); |
||||
} |
||||
return stringBuilder.append("===============").toString(); |
||||
} |
||||
} |
@ -0,0 +1,135 @@ |
||||
/* |
||||
* Copyright (c) 2009-2018 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
/** |
||||
* An array that can be dynamically modified |
||||
* @author Marcin Roguski |
||||
* @param <T> |
||||
* the type of stored data in the array |
||||
*/ |
||||
public class DynamicArray<T> implements Cloneable { |
||||
|
||||
/** An array object that holds the required data. */ |
||||
private T[] array; |
||||
/** |
||||
* This table holds the sizes of dimensions of the dynamic table. Its length specifies the table dimension or a |
||||
* pointer level. For example: if tableSizes.length == 3 then it either specifies a dynamic table of fixed lengths: |
||||
* dynTable[a][b][c], where a,b,c are stored in the tableSizes table. |
||||
*/ |
||||
private int[] tableSizes; |
||||
|
||||
/** |
||||
* Constructor. Builds an empty array of the specified sizes. |
||||
* @param tableSizes |
||||
* the sizes of the table |
||||
* @throws IllegalArgumentException |
||||
* an exception is thrown if one of the sizes is not a positive number |
||||
*/ |
||||
public DynamicArray(int[] tableSizes, T[] data) { |
||||
this.tableSizes = tableSizes; |
||||
int totalSize = 1; |
||||
for (int size : tableSizes) { |
||||
if (size <= 0) { |
||||
throw new IllegalArgumentException("The size of the table must be positive!"); |
||||
} |
||||
totalSize *= size; |
||||
} |
||||
if (totalSize != data.length) { |
||||
throw new IllegalArgumentException("The size of the table does not match the size of the given data!"); |
||||
} |
||||
this.array = data; |
||||
} |
||||
|
||||
@Override |
||||
public Object clone() throws CloneNotSupportedException { |
||||
return super.clone(); |
||||
} |
||||
|
||||
/** |
||||
* This method returns a value on the specified position. The dimension of the table is not taken into |
||||
* consideration. |
||||
* @param position |
||||
* the position of the data |
||||
* @return required data |
||||
*/ |
||||
public T get(int position) { |
||||
return array[position]; |
||||
} |
||||
|
||||
/** |
||||
* This method returns a value on the specified position in multidimensional array. Be careful not to exceed the |
||||
* table boundaries. Check the table's dimension first. |
||||
* @param position |
||||
* the position of the data indices of data position |
||||
* @return required data required data |
||||
*/ |
||||
public T get(int... position) { |
||||
if (position.length != tableSizes.length) { |
||||
throw new ArrayIndexOutOfBoundsException("The table accepts " + tableSizes.length + " indexing number(s)!"); |
||||
} |
||||
int index = 0; |
||||
for (int i = 0; i < position.length - 1; ++i) { |
||||
index += position[i] * tableSizes[i + 1]; |
||||
} |
||||
index += position[position.length - 1]; |
||||
return array[index]; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the total amount of data stored in the array. |
||||
* @return the total amount of data stored in the array |
||||
*/ |
||||
public int getTotalSize() { |
||||
return array.length; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder result = new StringBuilder(); |
||||
if (array instanceof Character[]) {// in case of character array we convert it to String
|
||||
for (int i = 0; i < array.length && (Character) array[i] != '\0'; ++i) {// strings are terminater with '0'
|
||||
result.append(array[i]); |
||||
} |
||||
} else { |
||||
result.append('['); |
||||
for (int i = 0; i < array.length; ++i) { |
||||
result.append(array[i].toString()); |
||||
if (i + 1 < array.length) { |
||||
result.append(','); |
||||
} |
||||
} |
||||
result.append(']'); |
||||
} |
||||
return result.toString(); |
||||
} |
||||
} |
@ -0,0 +1,327 @@ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.Structure.DataType; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* This class represents a single field in the structure. It can be either a primitive type or a table or a reference to |
||||
* another structure. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
/* package */ |
||||
class Field implements Cloneable { |
||||
|
||||
private static final int NAME_LENGTH = 24; |
||||
private static final int TYPE_LENGTH = 16; |
||||
/** The blender context. */ |
||||
public BlenderContext blenderContext; |
||||
/** The type of the field. */ |
||||
public String type; |
||||
/** The name of the field. */ |
||||
public String name; |
||||
/** The value of the field. Filled during data reading. */ |
||||
public Object value; |
||||
/** This variable indicates the level of the pointer. */ |
||||
public int pointerLevel; |
||||
/** |
||||
* This variable determines the sizes of the array. If the value is null then the field is not an array. |
||||
*/ |
||||
public int[] tableSizes; |
||||
/** This variable indicates if the field is a function pointer. */ |
||||
public boolean function; |
||||
|
||||
/** |
||||
* Constructor. Saves the field data and parses its name. |
||||
* @param name |
||||
* the name of the field |
||||
* @param type |
||||
* the type of the field |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown if the names contain errors |
||||
*/ |
||||
public Field(String name, String type, BlenderContext blenderContext) throws BlenderFileException { |
||||
this.type = type; |
||||
this.blenderContext = blenderContext; |
||||
this.parseField(new StringBuilder(name)); |
||||
} |
||||
|
||||
/** |
||||
* Copy constructor. Used in clone method. Copying is not full. The value in the new object is not set so that we |
||||
* have a clean empty copy of the field to fill with data. |
||||
* @param field |
||||
* the object that we copy |
||||
*/ |
||||
private Field(Field field) { |
||||
type = field.type; |
||||
name = field.name; |
||||
blenderContext = field.blenderContext; |
||||
pointerLevel = field.pointerLevel; |
||||
if (field.tableSizes != null) { |
||||
tableSizes = field.tableSizes.clone(); |
||||
} |
||||
function = field.function; |
||||
} |
||||
|
||||
@Override |
||||
public Object clone() throws CloneNotSupportedException { |
||||
return new Field(this); |
||||
} |
||||
|
||||
/** |
||||
* This method fills the field with data read from the input stream. |
||||
* @param blenderInputStream |
||||
* the stream we read data from |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when the blend file is somehow invalid or corrupted |
||||
*/ |
||||
public void fill(BlenderInputStream blenderInputStream) throws BlenderFileException { |
||||
int dataToRead = 1; |
||||
if (tableSizes != null && tableSizes.length > 0) { |
||||
for (int size : tableSizes) { |
||||
if (size <= 0) { |
||||
throw new BlenderFileException("The field " + name + " has invalid table size: " + size); |
||||
} |
||||
dataToRead *= size; |
||||
} |
||||
} |
||||
DataType dataType = pointerLevel == 0 ? DataType.getDataType(type, blenderContext) : DataType.POINTER; |
||||
switch (dataType) { |
||||
case POINTER: |
||||
if (dataToRead == 1) { |
||||
Pointer pointer = new Pointer(pointerLevel, function, blenderContext); |
||||
pointer.fill(blenderInputStream); |
||||
value = pointer; |
||||
} else { |
||||
Pointer[] data = new Pointer[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
Pointer pointer = new Pointer(pointerLevel, function, blenderContext); |
||||
pointer.fill(blenderInputStream); |
||||
data[i] = pointer; |
||||
} |
||||
value = new DynamicArray<Pointer>(tableSizes, data); |
||||
} |
||||
break; |
||||
case CHARACTER: |
||||
// character is also stored as a number, because sometimes the new blender version uses
|
||||
// other number type instead of character as a field type
|
||||
// and characters are very often used as byte number stores instead of real chars
|
||||
if (dataToRead == 1) { |
||||
value = Byte.valueOf((byte) blenderInputStream.readByte()); |
||||
} else { |
||||
Character[] data = new Character[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
data[i] = Character.valueOf((char) blenderInputStream.readByte()); |
||||
} |
||||
value = new DynamicArray<Character>(tableSizes, data); |
||||
} |
||||
break; |
||||
case SHORT: |
||||
if (dataToRead == 1) { |
||||
value = Integer.valueOf(blenderInputStream.readShort()); |
||||
} else { |
||||
Number[] data = new Number[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
data[i] = Integer.valueOf(blenderInputStream.readShort()); |
||||
} |
||||
value = new DynamicArray<Number>(tableSizes, data); |
||||
} |
||||
break; |
||||
case INTEGER: |
||||
if (dataToRead == 1) { |
||||
value = Integer.valueOf(blenderInputStream.readInt()); |
||||
} else { |
||||
Number[] data = new Number[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
data[i] = Integer.valueOf(blenderInputStream.readInt()); |
||||
} |
||||
value = new DynamicArray<Number>(tableSizes, data); |
||||
} |
||||
break; |
||||
case LONG: |
||||
if (dataToRead == 1) { |
||||
value = Long.valueOf(blenderInputStream.readLong()); |
||||
} else { |
||||
Number[] data = new Number[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
data[i] = Long.valueOf(blenderInputStream.readLong()); |
||||
} |
||||
value = new DynamicArray<Number>(tableSizes, data); |
||||
} |
||||
break; |
||||
case FLOAT: |
||||
if (dataToRead == 1) { |
||||
value = Float.valueOf(blenderInputStream.readFloat()); |
||||
} else { |
||||
Number[] data = new Number[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
data[i] = Float.valueOf(blenderInputStream.readFloat()); |
||||
} |
||||
value = new DynamicArray<Number>(tableSizes, data); |
||||
} |
||||
break; |
||||
case DOUBLE: |
||||
if (dataToRead == 1) { |
||||
value = Double.valueOf(blenderInputStream.readDouble()); |
||||
} else { |
||||
Number[] data = new Number[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
data[i] = Double.valueOf(blenderInputStream.readDouble()); |
||||
} |
||||
value = new DynamicArray<Number>(tableSizes, data); |
||||
} |
||||
break; |
||||
case VOID: |
||||
break; |
||||
case STRUCTURE: |
||||
if (dataToRead == 1) { |
||||
Structure structure = blenderContext.getDnaBlockData().getStructure(type); |
||||
structure.fill(blenderContext.getInputStream()); |
||||
value = structure; |
||||
} else { |
||||
Structure[] data = new Structure[dataToRead]; |
||||
for (int i = 0; i < dataToRead; ++i) { |
||||
Structure structure = blenderContext.getDnaBlockData().getStructure(type); |
||||
structure.fill(blenderContext.getInputStream()); |
||||
data[i] = structure; |
||||
} |
||||
value = new DynamicArray<Structure>(tableSizes, data); |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unimplemented filling of type: " + type); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method parses the field name to determine how the field should be used. |
||||
* @param nameBuilder |
||||
* the name of the field (given as StringBuilder) |
||||
* @throws BlenderFileException |
||||
* this exception is thrown if the names contain errors |
||||
*/ |
||||
private void parseField(StringBuilder nameBuilder) throws BlenderFileException { |
||||
this.removeWhitespaces(nameBuilder); |
||||
// veryfying if the name is a pointer
|
||||
int pointerIndex = nameBuilder.indexOf("*"); |
||||
while (pointerIndex >= 0) { |
||||
++pointerLevel; |
||||
nameBuilder.deleteCharAt(pointerIndex); |
||||
pointerIndex = nameBuilder.indexOf("*"); |
||||
} |
||||
// veryfying if the name is a function pointer
|
||||
if (nameBuilder.indexOf("(") >= 0) { |
||||
function = true; |
||||
this.removeCharacter(nameBuilder, '('); |
||||
this.removeCharacter(nameBuilder, ')'); |
||||
} else { |
||||
// veryfying if the name is a table
|
||||
int tableStartIndex = 0; |
||||
List<Integer> lengths = new ArrayList<Integer>(3);// 3 dimensions will be enough in most cases
|
||||
do { |
||||
tableStartIndex = nameBuilder.indexOf("["); |
||||
if (tableStartIndex > 0) { |
||||
int tableStopIndex = nameBuilder.indexOf("]"); |
||||
if (tableStopIndex < 0) { |
||||
throw new BlenderFileException("Invalid structure name: " + name); |
||||
} |
||||
try { |
||||
lengths.add(Integer.valueOf(nameBuilder.substring(tableStartIndex + 1, tableStopIndex))); |
||||
} catch (NumberFormatException e) { |
||||
throw new BlenderFileException("Invalid structure name caused by invalid table length: " + name, e); |
||||
} |
||||
nameBuilder.delete(tableStartIndex, tableStopIndex + 1); |
||||
} |
||||
} while (tableStartIndex > 0); |
||||
if (!lengths.isEmpty()) { |
||||
tableSizes = new int[lengths.size()]; |
||||
for (int i = 0; i < tableSizes.length; ++i) { |
||||
tableSizes[i] = lengths.get(i).intValue(); |
||||
} |
||||
} |
||||
} |
||||
name = nameBuilder.toString(); |
||||
} |
||||
|
||||
/** |
||||
* This method removes the required character from the text. |
||||
* @param text |
||||
* the text we remove characters from |
||||
* @param toRemove |
||||
* the character to be removed |
||||
*/ |
||||
private void removeCharacter(StringBuilder text, char toRemove) { |
||||
for (int i = 0; i < text.length(); ++i) { |
||||
if (text.charAt(i) == toRemove) { |
||||
text.deleteCharAt(i); |
||||
--i; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method removes all whitespace from the text. |
||||
* @param text |
||||
* the text we remove whitespace from |
||||
*/ |
||||
private void removeWhitespaces(StringBuilder text) { |
||||
for (int i = 0; i < text.length(); ++i) { |
||||
if (Character.isWhitespace(text.charAt(i))) { |
||||
text.deleteCharAt(i); |
||||
--i; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method builds the full name of the field (with function, pointer and table indications). |
||||
* @return the full name of the field |
||||
*/ |
||||
/*package*/ String getFullName() { |
||||
StringBuilder result = new StringBuilder(); |
||||
if (function) { |
||||
result.append('('); |
||||
} |
||||
for (int i = 0; i < pointerLevel; ++i) { |
||||
result.append('*'); |
||||
} |
||||
result.append(name); |
||||
if (tableSizes != null) { |
||||
for (int i = 0; i < tableSizes.length; ++i) { |
||||
result.append('[').append(tableSizes[i]).append(']'); |
||||
} |
||||
} |
||||
if (function) { |
||||
result.append(")()"); |
||||
} |
||||
return result.toString(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder result = new StringBuilder(); |
||||
result.append(this.getFullName()); |
||||
|
||||
// insert appropriate number of spaces to format the output corrently
|
||||
int nameLength = result.length(); |
||||
result.append(' ');// at least one space is a must
|
||||
for (int i = 1; i < NAME_LENGTH - nameLength; ++i) {// we start from i=1 because one space is already added
|
||||
result.append(' '); |
||||
} |
||||
result.append(type); |
||||
nameLength = result.length(); |
||||
for (int i = 0; i < NAME_LENGTH + TYPE_LENGTH - nameLength; ++i) { |
||||
result.append(' '); |
||||
} |
||||
if (value instanceof Character) { |
||||
result.append(" = ").append((int) ((Character) value).charValue()); |
||||
} else { |
||||
result.append(" = ").append(value != null ? value.toString() : "null"); |
||||
} |
||||
return result.toString(); |
||||
} |
||||
} |
@ -0,0 +1,213 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
|
||||
/** |
||||
* A class that holds the header data of a file block. The file block itself is not implemented. This class holds its |
||||
* start position in the stream and using this the structure can fill itself with the proper data. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class FileBlockHeader { |
||||
private static final Logger LOGGER = Logger.getLogger(FileBlockHeader.class.getName()); |
||||
|
||||
/** Identifier of the file-block [4 bytes]. */ |
||||
private BlockCode code; |
||||
/** Total length of the data after the file-block-header [4 bytes]. */ |
||||
private int size; |
||||
/** |
||||
* Memory address the structure was located when written to disk [4 or 8 bytes (defined in file header as a pointer |
||||
* size)]. |
||||
*/ |
||||
private long oldMemoryAddress; |
||||
/** Index of the SDNA structure [4 bytes]. */ |
||||
private int sdnaIndex; |
||||
/** Number of structure located in this file-block [4 bytes]. */ |
||||
private int count; |
||||
/** Start position of the block's data in the stream. */ |
||||
private int blockPosition; |
||||
|
||||
/** |
||||
* Constructor. Loads the block header from the given stream during instance creation. |
||||
* @param inputStream |
||||
* the stream we read the block header from |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the pointer size is neither 4 nor 8 |
||||
*/ |
||||
public FileBlockHeader(BlenderInputStream inputStream, BlenderContext blenderContext) throws BlenderFileException { |
||||
inputStream.alignPosition(4); |
||||
code = BlockCode.valueOf(inputStream.readByte() << 24 | inputStream.readByte() << 16 | inputStream.readByte() << 8 | inputStream.readByte()); |
||||
size = inputStream.readInt(); |
||||
oldMemoryAddress = inputStream.readPointer(); |
||||
sdnaIndex = inputStream.readInt(); |
||||
count = inputStream.readInt(); |
||||
blockPosition = inputStream.getPosition(); |
||||
if (BlockCode.BLOCK_DNA1 == code) { |
||||
blenderContext.setBlockData(new DnaBlockData(inputStream, blenderContext)); |
||||
} else { |
||||
inputStream.setPosition(blockPosition + size); |
||||
blenderContext.addFileBlockHeader(Long.valueOf(oldMemoryAddress), this); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the structure described by the header filled with appropriate data. |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return structure filled with data |
||||
* @throws BlenderFileException |
||||
*/ |
||||
public Structure getStructure(BlenderContext blenderContext) throws BlenderFileException { |
||||
blenderContext.getInputStream().setPosition(blockPosition); |
||||
Structure structure = blenderContext.getDnaBlockData().getStructure(sdnaIndex); |
||||
structure.fill(blenderContext.getInputStream()); |
||||
return structure; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the code of this data block. |
||||
* @return the code of this data block |
||||
*/ |
||||
public BlockCode getCode() { |
||||
return code; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the size of the data stored in this block. |
||||
* @return the size of the data stored in this block |
||||
*/ |
||||
public int getSize() { |
||||
return size; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the sdna index. |
||||
* @return the sdna index |
||||
*/ |
||||
public int getSdnaIndex() { |
||||
return sdnaIndex; |
||||
} |
||||
|
||||
/** |
||||
* This data returns the number of structure stored in the data block after this header. |
||||
* @return the number of structure stored in the data block after this header |
||||
*/ |
||||
public int getCount() { |
||||
return count; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the start position of the data block in the blend file stream. |
||||
* @return the start position of the data block |
||||
*/ |
||||
public int getBlockPosition() { |
||||
return blockPosition; |
||||
} |
||||
|
||||
/** |
||||
* This method indicates if the block is the last block in the file. |
||||
* @return true if this block is the last one in the file nad false otherwise |
||||
*/ |
||||
public boolean isLastBlock() { |
||||
return BlockCode.BLOCK_ENDB == code; |
||||
} |
||||
|
||||
/** |
||||
* This method indicates if the block is the SDNA block. |
||||
* @return true if this block is the SDNA block and false otherwise |
||||
*/ |
||||
public boolean isDnaBlock() { |
||||
return BlockCode.BLOCK_DNA1 == code; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "FILE BLOCK HEADER [" + code.toString() + " : " + size + " : " + oldMemoryAddress + " : " + sdnaIndex + " : " + count + "]"; |
||||
} |
||||
|
||||
public static enum BlockCode { |
||||
BLOCK_ME00('M' << 24 | 'E' << 16), // mesh
|
||||
BLOCK_CA00('C' << 24 | 'A' << 16), // camera
|
||||
BLOCK_LA00('L' << 24 | 'A' << 16), // lamp
|
||||
BLOCK_OB00('O' << 24 | 'B' << 16), // object
|
||||
BLOCK_MA00('M' << 24 | 'A' << 16), // material
|
||||
BLOCK_SC00('S' << 24 | 'C' << 16), // scene
|
||||
BLOCK_WO00('W' << 24 | 'O' << 16), // world
|
||||
BLOCK_TX00('T' << 24 | 'X' << 16), // texture
|
||||
BLOCK_IP00('I' << 24 | 'P' << 16), // ipo
|
||||
BLOCK_AC00('A' << 24 | 'C' << 16), // action
|
||||
BLOCK_IM00('I' << 24 | 'M' << 16), // image
|
||||
BLOCK_TE00('T' << 24 | 'E' << 16), |
||||
BLOCK_WM00('W' << 24 | 'M' << 16), |
||||
BLOCK_SR00('S' << 24 | 'R' << 16), |
||||
BLOCK_SN00('S' << 24 | 'N' << 16), |
||||
BLOCK_BR00('B' << 24 | 'R' << 16), |
||||
BLOCK_LS00('L' << 24 | 'S' << 16), |
||||
BLOCK_GR00('G' << 24 | 'R' << 16), |
||||
BLOCK_AR00('A' << 24 | 'R' << 16), |
||||
BLOCK_GLOB('G' << 24 | 'L' << 16 | 'O' << 8 | 'B'), |
||||
BLOCK_REND('R' << 24 | 'E' << 16 | 'N' << 8 | 'D'), |
||||
BLOCK_DATA('D' << 24 | 'A' << 16 | 'T' << 8 | 'A'), |
||||
BLOCK_DNA1('D' << 24 | 'N' << 16 | 'A' << 8 | '1'), |
||||
BLOCK_ENDB('E' << 24 | 'N' << 16 | 'D' << 8 | 'B'), |
||||
BLOCK_TEST('T' << 24 | 'E' << 16 | 'S' << 8 | 'T'), |
||||
BLOCK_UNKN(0); |
||||
|
||||
private int code; |
||||
|
||||
private BlockCode(int code) { |
||||
this.code = code; |
||||
} |
||||
|
||||
public static BlockCode valueOf(int code) { |
||||
for (BlockCode blockCode : BlockCode.values()) { |
||||
if (blockCode.code == code) { |
||||
return blockCode; |
||||
} |
||||
} |
||||
byte[] codeBytes = new byte[] { (byte) (code >> 24 & 0xFF), (byte) (code >> 16 & 0xFF), (byte) (code >> 8 & 0xFF), (byte) (code & 0xFF) }; |
||||
for (int i = 0; i < codeBytes.length; ++i) { |
||||
if (codeBytes[i] == 0) { |
||||
codeBytes[i] = '0'; |
||||
} |
||||
} |
||||
LOGGER.log(Level.WARNING, "Unknown block header: {0}", new String(codeBytes)); |
||||
return BLOCK_UNKN; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,189 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
|
||||
/** |
||||
* A class that represents a pointer of any level that can be stored in the file. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class Pointer { |
||||
|
||||
/** The blender context. */ |
||||
private BlenderContext blenderContext; |
||||
/** The level of the pointer. */ |
||||
private int pointerLevel; |
||||
/** The address in file it points to. */ |
||||
private long oldMemoryAddress; |
||||
/** This variable indicates if the field is a function pointer. */ |
||||
public boolean function; |
||||
|
||||
/** |
||||
* Constructr. Stores the basic data about the pointer. |
||||
* @param pointerLevel |
||||
* the level of the pointer |
||||
* @param function |
||||
* this variable indicates if the field is a function pointer |
||||
* @param blenderContext |
||||
* the repository f data; used in fetching the value that the pointer points |
||||
*/ |
||||
public Pointer(int pointerLevel, boolean function, BlenderContext blenderContext) { |
||||
this.pointerLevel = pointerLevel; |
||||
this.function = function; |
||||
this.blenderContext = blenderContext; |
||||
} |
||||
|
||||
/** |
||||
* This method fills the pointer with its address value (it doesn't get the actual data yet. Use the 'fetch' method |
||||
* for this. |
||||
* @param inputStream |
||||
* the stream we read the pointer value from |
||||
*/ |
||||
public void fill(BlenderInputStream inputStream) { |
||||
oldMemoryAddress = inputStream.readPointer(); |
||||
} |
||||
|
||||
/** |
||||
* This method fetches the data stored under the given address. |
||||
* @return the data read from the file |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is somehow invalid or corrupted |
||||
*/ |
||||
public List<Structure> fetchData() throws BlenderFileException { |
||||
if (oldMemoryAddress == 0) { |
||||
throw new NullPointerException("The pointer points to nothing!"); |
||||
} |
||||
List<Structure> structures = null; |
||||
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(oldMemoryAddress); |
||||
if (dataFileBlock == null) { |
||||
throw new BlenderFileException("No data stored for address: " + oldMemoryAddress + ". Make sure you did not open the newer blender file with older blender version."); |
||||
} |
||||
BlenderInputStream inputStream = blenderContext.getInputStream(); |
||||
if (pointerLevel > 1) { |
||||
int pointersAmount = dataFileBlock.getSize() / inputStream.getPointerSize() * dataFileBlock.getCount(); |
||||
for (int i = 0; i < pointersAmount; ++i) { |
||||
inputStream.setPosition(dataFileBlock.getBlockPosition() + inputStream.getPointerSize() * i); |
||||
long oldMemoryAddress = inputStream.readPointer(); |
||||
if (oldMemoryAddress != 0L) { |
||||
Pointer p = new Pointer(pointerLevel - 1, function, blenderContext); |
||||
p.oldMemoryAddress = oldMemoryAddress; |
||||
if (structures == null) { |
||||
structures = p.fetchData(); |
||||
} else { |
||||
structures.addAll(p.fetchData()); |
||||
} |
||||
} else { |
||||
// it is necessary to put null's if the pointer is null, ie. in materials array that is attached to the mesh, the index
|
||||
// of the material is important, that is why we need null's to indicate that some materials' slots are empty
|
||||
if (structures == null) { |
||||
structures = new ArrayList<Structure>(); |
||||
} |
||||
structures.add(null); |
||||
} |
||||
} |
||||
} else { |
||||
inputStream.setPosition(dataFileBlock.getBlockPosition()); |
||||
structures = new ArrayList<Structure>(dataFileBlock.getCount()); |
||||
for (int i = 0; i < dataFileBlock.getCount(); ++i) { |
||||
Structure structure = blenderContext.getDnaBlockData().getStructure(dataFileBlock.getSdnaIndex()); |
||||
structure.fill(blenderContext.getInputStream()); |
||||
structures.add(structure); |
||||
} |
||||
return structures; |
||||
} |
||||
return structures; |
||||
} |
||||
|
||||
/** |
||||
* This method indicates if this pointer points to a function. |
||||
* @return <b>true</b> if this is a function pointer and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isFunction() { |
||||
return function; |
||||
} |
||||
|
||||
/** |
||||
* This method indicates if this is a null-pointer or not. |
||||
* @return <b>true</b> if the pointer is null and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isNull() { |
||||
return oldMemoryAddress == 0; |
||||
} |
||||
|
||||
/** |
||||
* This method indicates if this is a null-pointer or not. |
||||
* @return <b>true</b> if the pointer is not null and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isNotNull() { |
||||
return oldMemoryAddress != 0; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the old memory address of the structure pointed by the pointer. |
||||
* @return the old memory address of the structure pointed by the pointer |
||||
*/ |
||||
public long getOldMemoryAddress() { |
||||
return oldMemoryAddress; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return oldMemoryAddress == 0 ? "{$null$}" : "{$" + oldMemoryAddress + "$}"; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return 31 + (int) (oldMemoryAddress ^ oldMemoryAddress >>> 32); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (obj == null) { |
||||
return false; |
||||
} |
||||
if (this.getClass() != obj.getClass()) { |
||||
return false; |
||||
} |
||||
Pointer other = (Pointer) obj; |
||||
if (oldMemoryAddress != other.oldMemoryAddress) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,328 @@ |
||||
/* |
||||
* Copyright (c) 2009-2018 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.file; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
|
||||
/** |
||||
* A class representing a single structure in the file. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class Structure implements Cloneable { |
||||
|
||||
/** The address of the block that fills the structure. */ |
||||
private transient Long oldMemoryAddress; |
||||
/** The type of the structure. */ |
||||
private String type; |
||||
/** |
||||
* The fields of the structure. Each field consists of a pair: name-type. |
||||
*/ |
||||
private Field[] fields; |
||||
|
||||
/** |
||||
* Constructor that copies the data of the structure. |
||||
* @param structure |
||||
* the structure to copy. |
||||
* @throws CloneNotSupportedException |
||||
* this exception should never be thrown |
||||
*/ |
||||
private Structure(Structure structure) throws CloneNotSupportedException { |
||||
type = structure.type; |
||||
fields = new Field[structure.fields.length]; |
||||
for (int i = 0; i < fields.length; ++i) { |
||||
fields[i] = (Field) structure.fields[i].clone(); |
||||
} |
||||
oldMemoryAddress = structure.oldMemoryAddress; |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Loads the structure from the given stream during instance creation. |
||||
* @param inputStream |
||||
* the stream we read the structure from |
||||
* @param names |
||||
* the names from which the name of structure and its fields will be taken |
||||
* @param types |
||||
* the names of types for the structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception occurs if the amount of fields, defined in the file, is negative |
||||
*/ |
||||
public Structure(BlenderInputStream inputStream, String[] names, String[] types, BlenderContext blenderContext) throws BlenderFileException { |
||||
int nameIndex = inputStream.readShort(); |
||||
type = types[nameIndex]; |
||||
int fieldsAmount = inputStream.readShort(); |
||||
if (fieldsAmount < 0) { |
||||
throw new BlenderFileException("The amount of fields of " + type + " structure cannot be negative!"); |
||||
} |
||||
if (fieldsAmount > 0) { |
||||
fields = new Field[fieldsAmount]; |
||||
for (int i = 0; i < fieldsAmount; ++i) { |
||||
int typeIndex = inputStream.readShort(); |
||||
nameIndex = inputStream.readShort(); |
||||
fields[i] = new Field(names[nameIndex], types[typeIndex], blenderContext); |
||||
} |
||||
} |
||||
oldMemoryAddress = Long.valueOf(-1L); |
||||
} |
||||
|
||||
/** |
||||
* This method fills the structure with data. |
||||
* @param inputStream |
||||
* the stream we read data from, its read cursor should be placed at the start position of the data for the |
||||
* structure |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when the blend file is somehow invalid or corrupted |
||||
*/ |
||||
public void fill(BlenderInputStream inputStream) throws BlenderFileException { |
||||
int position = inputStream.getPosition(); |
||||
inputStream.setPosition(position - 8 - inputStream.getPointerSize()); |
||||
oldMemoryAddress = Long.valueOf(inputStream.readPointer()); |
||||
inputStream.setPosition(position); |
||||
for (Field field : fields) { |
||||
field.fill(inputStream); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the value of the filed with a given name. |
||||
* @param fieldName |
||||
* the name of the field |
||||
* @return the value of the field or null if no field with a given name is found |
||||
*/ |
||||
public Object getFieldValue(String fieldName) { |
||||
return this.getFieldValue(fieldName, null); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the value of the filed with a given name. |
||||
* @param fieldName |
||||
* the name of the field |
||||
* @param defaultValue |
||||
* the value that is being returned when no field of a given name is found |
||||
* @return the value of the field or the given default value if no field with a given name is found |
||||
*/ |
||||
public Object getFieldValue(String fieldName, Object defaultValue) { |
||||
for (Field field : fields) { |
||||
if (field.name.equalsIgnoreCase(fieldName)) { |
||||
return field.value; |
||||
} |
||||
} |
||||
return defaultValue; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the value of the filed with a given name. The structure is considered to have flat fields |
||||
* only (no substructures). |
||||
* @param fieldName |
||||
* the name of the field |
||||
* @return the value of the field or null if no field with a given name is found |
||||
*/ |
||||
public Object getFlatFieldValue(String fieldName) { |
||||
for (Field field : fields) { |
||||
Object value = field.value; |
||||
if (field.name.equalsIgnoreCase(fieldName)) { |
||||
return value; |
||||
} else if (value instanceof Structure) { |
||||
value = ((Structure) value).getFlatFieldValue(fieldName); |
||||
if (value != null) {// we can compare references here, since we use one static object as a NULL field value
|
||||
return value; |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* This method should be used on structures that are of a 'ListBase' type. It creates a List of structures that are |
||||
* held by this structure within the blend file. |
||||
* @return a list of filled structures |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is somehow invalid or corrupted |
||||
* @throws IllegalArgumentException |
||||
* this exception is thrown if the type of the structure is not 'ListBase' |
||||
*/ |
||||
public List<Structure> evaluateListBase() throws BlenderFileException { |
||||
if (!"ListBase".equals(type)) { |
||||
throw new IllegalStateException("This structure is not of type: 'ListBase'"); |
||||
} |
||||
Pointer first = (Pointer) this.getFieldValue("first"); |
||||
Pointer last = (Pointer) this.getFieldValue("last"); |
||||
long currentAddress = 0; |
||||
long lastAddress = last.getOldMemoryAddress(); |
||||
List<Structure> result = new LinkedList<Structure>(); |
||||
while (currentAddress != lastAddress) { |
||||
currentAddress = first.getOldMemoryAddress(); |
||||
Structure structure = first.fetchData().get(0); |
||||
result.add(structure); |
||||
first = (Pointer) structure.getFlatFieldValue("next"); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the type of the structure. |
||||
* @return the type of the structure |
||||
*/ |
||||
public String getType() { |
||||
return type; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the amount of fields for the current structure. |
||||
* @return the amount of fields for the current structure |
||||
*/ |
||||
public int getFieldsAmount() { |
||||
return fields.length; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the full field name of the given index. |
||||
* @param fieldIndex |
||||
* the index of the field |
||||
* @return the full field name of the given index |
||||
*/ |
||||
public String getFieldFullName(int fieldIndex) { |
||||
return fields[fieldIndex].getFullName(); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the field type of the given index. |
||||
* @param fieldIndex |
||||
* the index of the field |
||||
* @return the field type of the given index |
||||
*/ |
||||
public String getFieldType(int fieldIndex) { |
||||
return fields[fieldIndex].type; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the address of the structure. The structure should be filled with data otherwise an exception |
||||
* is thrown. |
||||
* @return the address of the feature stored in this structure |
||||
*/ |
||||
public Long getOldMemoryAddress() { |
||||
if (oldMemoryAddress.longValue() == -1L) { |
||||
throw new IllegalStateException("Call the 'fill' method and fill the structure with data first!"); |
||||
} |
||||
return oldMemoryAddress; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the name of the structure. If the structure has an ID field then the name is returned. |
||||
* Otherwise the name does not exists and the method returns null. |
||||
* @return the name of the structure read from the ID field or null |
||||
*/ |
||||
public String getName() { |
||||
Object fieldValue = this.getFieldValue("ID"); |
||||
if (fieldValue instanceof Structure) { |
||||
Structure id = (Structure) fieldValue; |
||||
return id == null ? null : id.getFieldValue("name").toString().substring(2);// blender adds 2-charactes as a name prefix
|
||||
} |
||||
Object name = this.getFieldValue("name", null); |
||||
return name == null ? null : name.toString().substring(2); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder result = new StringBuilder("struct ").append(type).append(" {\n"); |
||||
for (int i = 0; i < fields.length; ++i) { |
||||
result.append(fields[i].toString()).append('\n'); |
||||
} |
||||
return result.append('}').toString(); |
||||
} |
||||
|
||||
@Override |
||||
public Object clone() throws CloneNotSupportedException { |
||||
return new Structure(this); |
||||
} |
||||
|
||||
/** |
||||
* This enum enumerates all known data types that can be found in the blend file. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */static enum DataType { |
||||
|
||||
CHARACTER, SHORT, INTEGER, LONG, FLOAT, DOUBLE, VOID, STRUCTURE, POINTER; |
||||
/** The map containing the known primary types. */ |
||||
private static final Map<String, DataType> PRIMARY_TYPES = new HashMap<String, DataType>(10); |
||||
|
||||
static { |
||||
PRIMARY_TYPES.put("char", CHARACTER); |
||||
PRIMARY_TYPES.put("uchar", CHARACTER); |
||||
PRIMARY_TYPES.put("short", SHORT); |
||||
PRIMARY_TYPES.put("ushort", SHORT); |
||||
PRIMARY_TYPES.put("int", INTEGER); |
||||
PRIMARY_TYPES.put("long", LONG); |
||||
PRIMARY_TYPES.put("ulong", LONG); |
||||
PRIMARY_TYPES.put("uint64_t", LONG); |
||||
PRIMARY_TYPES.put("float", FLOAT); |
||||
PRIMARY_TYPES.put("double", DOUBLE); |
||||
PRIMARY_TYPES.put("void", VOID); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the data type that is appropriate to the given type name. WARNING! The type recognition |
||||
* is case sensitive! |
||||
* @param type |
||||
* the type name of the data |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return appropriate enum value to the given type name |
||||
* @throws BlenderFileException |
||||
* this exception is thrown if the given type name does not exist in the blend file |
||||
*/ |
||||
public static DataType getDataType(String type, BlenderContext blenderContext) throws BlenderFileException { |
||||
DataType result = PRIMARY_TYPES.get(type); |
||||
if (result != null) { |
||||
return result; |
||||
} |
||||
if (blenderContext.getDnaBlockData().hasStructure(type)) { |
||||
return STRUCTURE; |
||||
} |
||||
throw new BlenderFileException("Unknown data type: " + type); |
||||
} |
||||
|
||||
/** |
||||
* @return a collection of known primary types names |
||||
*/ |
||||
/* package */static Collection<String> getKnownPrimaryTypesNames() { |
||||
return PRIMARY_TYPES.keySet(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,214 @@ |
||||
package com.jme3.scene.plugins.blender.landscape; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.light.AmbientLight; |
||||
import com.jme3.light.Light; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.post.filters.FogFilter; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.textures.ColorBand; |
||||
import com.jme3.scene.plugins.blender.textures.CombinedTexture; |
||||
import com.jme3.scene.plugins.blender.textures.ImageUtils; |
||||
import com.jme3.scene.plugins.blender.textures.TextureHelper; |
||||
import com.jme3.scene.plugins.blender.textures.TexturePixel; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Image.Format; |
||||
import com.jme3.texture.TextureCubeMap; |
||||
import com.jme3.util.SkyFactory; |
||||
|
||||
/** |
||||
* The class that allows to load the following: <li>the ambient light of the scene <li>the sky of the scene (with or without texture) |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class LandscapeHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(LandscapeHelper.class.getName()); |
||||
|
||||
private static final int SKYTYPE_BLEND = 1; |
||||
private static final int SKYTYPE_REAL = 2; |
||||
private static final int SKYTYPE_PAPER = 4; |
||||
|
||||
private static final int MODE_MIST = 0x01; |
||||
|
||||
public LandscapeHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* Loads scene ambient light. |
||||
* @param worldStructure |
||||
* the world's blender structure |
||||
* @return the scene's ambient light |
||||
*/ |
||||
public Light toAmbientLight(Structure worldStructure) { |
||||
LOGGER.fine("Loading ambient light."); |
||||
AmbientLight ambientLight = null; |
||||
float ambr = ((Number) worldStructure.getFieldValue("ambr")).floatValue(); |
||||
float ambg = ((Number) worldStructure.getFieldValue("ambg")).floatValue(); |
||||
float ambb = ((Number) worldStructure.getFieldValue("ambb")).floatValue(); |
||||
if (ambr > 0 || ambg > 0 || ambb > 0) { |
||||
ambientLight = new AmbientLight(); |
||||
ColorRGBA ambientLightColor = new ColorRGBA(ambr, ambg, ambb, 0.0f); |
||||
ambientLight.setColor(ambientLightColor); |
||||
LOGGER.log(Level.FINE, "Loaded ambient light: {0}.", ambientLightColor); |
||||
} else { |
||||
LOGGER.finer("Ambient light is set to BLACK which means: no ambient light! The ambient light node will not be included in the result."); |
||||
} |
||||
return ambientLight; |
||||
} |
||||
|
||||
/** |
||||
* The method loads fog for the scene. |
||||
* NOTICE! Remember to manually set the distance and density of the fog. |
||||
* Unfortunately blender's fog parameters in no way fit to the JME. |
||||
* @param worldStructure |
||||
* the world's structure |
||||
* @return fog filter or null if scene does not define it |
||||
*/ |
||||
public FogFilter toFog(Structure worldStructure) { |
||||
FogFilter result = null; |
||||
int mode = ((Number) worldStructure.getFieldValue("mode")).intValue(); |
||||
if ((mode & MODE_MIST) != 0) { |
||||
LOGGER.fine("Loading fog."); |
||||
result = new FogFilter(); |
||||
result.setName("FIfog"); |
||||
result.setFogColor(this.toBackgroundColor(worldStructure)); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Loads the background color. |
||||
* @param worldStructure |
||||
* the world's structure |
||||
* @return the horizon color of the world which is used as a background color. |
||||
*/ |
||||
public ColorRGBA toBackgroundColor(Structure worldStructure) { |
||||
float horr = ((Number) worldStructure.getFieldValue("horr")).floatValue(); |
||||
float horg = ((Number) worldStructure.getFieldValue("horg")).floatValue(); |
||||
float horb = ((Number) worldStructure.getFieldValue("horb")).floatValue(); |
||||
return new ColorRGBA(horr, horg, horb, 1); |
||||
} |
||||
|
||||
/** |
||||
* Loads scene's sky. Sky can be plain or textured. |
||||
* If no sky type is selected in blender then no sky is loaded. |
||||
* @param worldStructure |
||||
* the world's structure |
||||
* @return the scene's sky |
||||
* @throws BlenderFileException |
||||
* blender exception is thrown when problems with blender file occur |
||||
*/ |
||||
public Spatial toSky(Structure worldStructure) throws BlenderFileException { |
||||
int skytype = ((Number) worldStructure.getFieldValue("skytype")).intValue(); |
||||
if (skytype == 0) { |
||||
return null; |
||||
} |
||||
|
||||
LOGGER.fine("Loading sky."); |
||||
ColorRGBA horizontalColor = this.toBackgroundColor(worldStructure); |
||||
|
||||
float zenr = ((Number) worldStructure.getFieldValue("zenr")).floatValue(); |
||||
float zeng = ((Number) worldStructure.getFieldValue("zeng")).floatValue(); |
||||
float zenb = ((Number) worldStructure.getFieldValue("zenb")).floatValue(); |
||||
ColorRGBA zenithColor = new ColorRGBA(zenr, zeng, zenb, 1); |
||||
|
||||
// jutr for this case load generated textures wheather user had set it or not because those might be needed to properly load the sky
|
||||
boolean loadGeneratedTextures = blenderContext.getBlenderKey().isLoadGeneratedTextures(); |
||||
blenderContext.getBlenderKey().setLoadGeneratedTextures(true); |
||||
|
||||
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class); |
||||
List<CombinedTexture> loadedTextures = null; |
||||
try { |
||||
loadedTextures = textureHelper.readTextureData(worldStructure, new float[] { horizontalColor.r, horizontalColor.g, horizontalColor.b, horizontalColor.a }, true); |
||||
} finally { |
||||
blenderContext.getBlenderKey().setLoadGeneratedTextures(loadGeneratedTextures); |
||||
} |
||||
|
||||
TextureCubeMap texture = null; |
||||
if (loadedTextures != null && loadedTextures.size() > 0) { |
||||
if (loadedTextures.size() > 1) { |
||||
throw new IllegalStateException("There should be only one combined texture for sky!"); |
||||
} |
||||
CombinedTexture combinedTexture = loadedTextures.get(0); |
||||
texture = combinedTexture.generateSkyTexture(horizontalColor, zenithColor, blenderContext); |
||||
} else { |
||||
LOGGER.fine("Preparing colors for colorband."); |
||||
int colorbandType = ColorBand.IPO_CARDINAL; |
||||
List<ColorRGBA> colorbandColors = new ArrayList<ColorRGBA>(3); |
||||
colorbandColors.add(horizontalColor); |
||||
if ((skytype & SKYTYPE_BLEND) != 0) { |
||||
if ((skytype & SKYTYPE_PAPER) != 0) { |
||||
colorbandType = ColorBand.IPO_LINEAR; |
||||
} |
||||
if ((skytype & SKYTYPE_REAL) != 0) { |
||||
colorbandColors.add(0, zenithColor); |
||||
} |
||||
colorbandColors.add(zenithColor); |
||||
} |
||||
|
||||
int size = blenderContext.getBlenderKey().getSkyGeneratedTextureSize(); |
||||
|
||||
List<Integer> positions = new ArrayList<Integer>(colorbandColors.size()); |
||||
positions.add(0); |
||||
if (colorbandColors.size() == 2) { |
||||
positions.add(size - 1); |
||||
} else if (colorbandColors.size() == 3) { |
||||
positions.add(size / 2); |
||||
positions.add(size - 1); |
||||
} |
||||
|
||||
LOGGER.fine("Generating sky texture."); |
||||
float[][] values = new ColorBand(colorbandType, colorbandColors, positions, size).computeValues(); |
||||
|
||||
Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6); |
||||
PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
|
||||
LOGGER.fine("Creating side textures."); |
||||
int[] sideImagesIndexes = new int[] { 0, 1, 4, 5 }; |
||||
for (int i : sideImagesIndexes) { |
||||
for (int y = 0; y < size; ++y) { |
||||
pixel.red = values[y][0]; |
||||
pixel.green = values[y][1]; |
||||
pixel.blue = values[y][2]; |
||||
|
||||
for (int x = 0; x < size; ++x) { |
||||
pixelIO.write(image, i, pixel, x, y); |
||||
} |
||||
} |
||||
} |
||||
|
||||
LOGGER.fine("Creating top texture."); |
||||
pixelIO.read(image, 0, pixel, 0, image.getHeight() - 1); |
||||
for (int y = 0; y < size; ++y) { |
||||
for (int x = 0; x < size; ++x) { |
||||
pixelIO.write(image, 3, pixel, x, y); |
||||
} |
||||
} |
||||
|
||||
LOGGER.fine("Creating bottom texture."); |
||||
pixelIO.read(image, 0, pixel, 0, 0); |
||||
for (int y = 0; y < size; ++y) { |
||||
for (int x = 0; x < size; ++x) { |
||||
pixelIO.write(image, 2, pixel, x, y); |
||||
} |
||||
} |
||||
|
||||
texture = new TextureCubeMap(image); |
||||
} |
||||
|
||||
LOGGER.fine("Sky texture created. Creating sky."); |
||||
return SkyFactory.createSky(blenderContext.getAssetManager(), texture, SkyFactory.EnvMapType.CubeMap); |
||||
} |
||||
} |
@ -0,0 +1,116 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.lights; |
||||
|
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.light.DirectionalLight; |
||||
import com.jme3.light.Light; |
||||
import com.jme3.light.PointLight; |
||||
import com.jme3.light.SpotLight; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A class that is used in light calculations. |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class LightHelper extends AbstractBlenderHelper { |
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(LightHelper.class.getName()); |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in |
||||
* different blender versions. |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public LightHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
public Light toLight(Structure structure, BlenderContext blenderContext) throws BlenderFileException { |
||||
Light result = (Light) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedDataType.FEATURE); |
||||
if (result != null) { |
||||
return result; |
||||
} |
||||
Light light = null; |
||||
int type = ((Number) structure.getFieldValue("type")).intValue(); |
||||
switch (type) { |
||||
case 0:// Lamp
|
||||
light = new PointLight(); |
||||
float distance = ((Number) structure.getFieldValue("dist")).floatValue(); |
||||
((PointLight) light).setRadius(distance); |
||||
break; |
||||
case 1:// Sun
|
||||
LOGGER.log(Level.WARNING, "'Sun' lamp is not supported in jMonkeyEngine. Using PointLight with radius = Float.MAX_VALUE."); |
||||
light = new PointLight(); |
||||
((PointLight) light).setRadius(Float.MAX_VALUE); |
||||
break; |
||||
case 2:// Spot
|
||||
light = new SpotLight(); |
||||
// range
|
||||
((SpotLight) light).setSpotRange(((Number) structure.getFieldValue("dist")).floatValue()); |
||||
// outer angle
|
||||
float outerAngle = ((Number) structure.getFieldValue("spotsize")).floatValue() * FastMath.DEG_TO_RAD * 0.5f; |
||||
((SpotLight) light).setSpotOuterAngle(outerAngle); |
||||
|
||||
// inner angle
|
||||
float spotblend = ((Number) structure.getFieldValue("spotblend")).floatValue(); |
||||
spotblend = FastMath.clamp(spotblend, 0, 1); |
||||
float innerAngle = outerAngle * (1 - spotblend); |
||||
((SpotLight) light).setSpotInnerAngle(innerAngle); |
||||
break; |
||||
case 3:// Hemi
|
||||
LOGGER.log(Level.WARNING, "'Hemi' lamp is not supported in jMonkeyEngine. Using DirectionalLight instead."); |
||||
case 4:// Area
|
||||
light = new DirectionalLight(); |
||||
break; |
||||
default: |
||||
throw new BlenderFileException("Unknown light source type: " + type); |
||||
} |
||||
float r = ((Number) structure.getFieldValue("r")).floatValue(); |
||||
float g = ((Number) structure.getFieldValue("g")).floatValue(); |
||||
float b = ((Number) structure.getFieldValue("b")).floatValue(); |
||||
light.setColor(new ColorRGBA(r, g, b, 1.0f)); |
||||
light.setName(structure.getName()); |
||||
return light; |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
package com.jme3.scene.plugins.blender.materials; |
||||
|
||||
/** |
||||
* An interface used in calculating alpha mask during particles' texture calculations. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */interface IAlphaMask { |
||||
/** |
||||
* This method sets the size of the texture's image. |
||||
* @param width |
||||
* the width of the image |
||||
* @param height |
||||
* the height of the image |
||||
*/ |
||||
void setImageSize(int width, int height); |
||||
|
||||
/** |
||||
* This method returns the alpha value for the specified texture position. |
||||
* @param x |
||||
* the X coordinate of the texture position |
||||
* @param y |
||||
* the Y coordinate of the texture position |
||||
* @return the alpha value for the specified texture position |
||||
*/ |
||||
byte getAlpha(float x, float y); |
||||
} |
@ -0,0 +1,366 @@ |
||||
package com.jme3.scene.plugins.blender.materials; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.export.JmeExporter; |
||||
import com.jme3.export.JmeImporter; |
||||
import com.jme3.export.Savable; |
||||
import com.jme3.material.Material; |
||||
import com.jme3.material.RenderState.BlendMode; |
||||
import com.jme3.material.RenderState.FaceCullMode; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.renderer.queue.RenderQueue.Bucket; |
||||
import com.jme3.scene.Geometry; |
||||
import com.jme3.scene.VertexBuffer; |
||||
import com.jme3.scene.VertexBuffer.Format; |
||||
import com.jme3.scene.VertexBuffer.Usage; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialHelper.DiffuseShader; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialHelper.SpecularShader; |
||||
import com.jme3.scene.plugins.blender.textures.CombinedTexture; |
||||
import com.jme3.scene.plugins.blender.textures.TextureHelper; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
/** |
||||
* This class holds the data about the material. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public final class MaterialContext implements Savable { |
||||
private static final Logger LOGGER = Logger.getLogger(MaterialContext.class.getName()); |
||||
|
||||
// texture mapping types
|
||||
public static final int MTEX_COL = 0x01; |
||||
public static final int MTEX_NOR = 0x02; |
||||
public static final int MTEX_SPEC = 0x04; |
||||
public static final int MTEX_EMIT = 0x40; |
||||
public static final int MTEX_ALPHA = 0x80; |
||||
public static final int MTEX_AMB = 0x800; |
||||
|
||||
public static final int FLAG_TRANSPARENT = 0x10000; |
||||
|
||||
/* package */final String name; |
||||
/* package */final List<CombinedTexture> loadedTextures; |
||||
|
||||
/* package */final ColorRGBA diffuseColor; |
||||
/* package */final DiffuseShader diffuseShader; |
||||
/* package */final SpecularShader specularShader; |
||||
/* package */final ColorRGBA specularColor; |
||||
/* package */final float ambientFactor; |
||||
/* package */final float shininess; |
||||
/* package */final boolean shadeless; |
||||
/* package */final boolean vertexColor; |
||||
/* package */final boolean transparent; |
||||
/* package */final boolean vTangent; |
||||
/* package */FaceCullMode faceCullMode; |
||||
|
||||
/* package */MaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException { |
||||
name = structure.getName(); |
||||
|
||||
int mode = ((Number) structure.getFieldValue("mode")).intValue(); |
||||
shadeless = (mode & 0x4) != 0; |
||||
vertexColor = (mode & 0x80) != 0; |
||||
vTangent = (mode & 0x4000000) != 0; // NOTE: Requires tangents
|
||||
|
||||
int diff_shader = ((Number) structure.getFieldValue("diff_shader")).intValue(); |
||||
diffuseShader = DiffuseShader.values()[diff_shader]; |
||||
ambientFactor = ((Number) structure.getFieldValue("amb")).floatValue(); |
||||
|
||||
if (shadeless) { |
||||
float r = ((Number) structure.getFieldValue("r")).floatValue(); |
||||
float g = ((Number) structure.getFieldValue("g")).floatValue(); |
||||
float b = ((Number) structure.getFieldValue("b")).floatValue(); |
||||
float alpha = ((Number) structure.getFieldValue("alpha")).floatValue(); |
||||
|
||||
diffuseColor = new ColorRGBA(r, g, b, alpha); |
||||
specularShader = null; |
||||
specularColor = null; |
||||
shininess = 0.0f; |
||||
} else { |
||||
diffuseColor = this.readDiffuseColor(structure, diffuseShader); |
||||
|
||||
int spec_shader = ((Number) structure.getFieldValue("spec_shader")).intValue(); |
||||
specularShader = SpecularShader.values()[spec_shader]; |
||||
specularColor = this.readSpecularColor(structure); |
||||
float shininess = ((Number) structure.getFieldValue("har")).floatValue();// this is (probably) the specular hardness in blender
|
||||
this.shininess = shininess > 0.0f ? shininess : MaterialHelper.DEFAULT_SHININESS; |
||||
} |
||||
|
||||
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class); |
||||
loadedTextures = textureHelper.readTextureData(structure, new float[] { diffuseColor.r, diffuseColor.g, diffuseColor.b, diffuseColor.a }, false); |
||||
|
||||
long flag = ((Number)structure.getFieldValue("flag")).longValue(); |
||||
if((flag & FLAG_TRANSPARENT) != 0) { |
||||
// veryfying if the transparency is present
|
||||
// (in blender transparent mask is 0x10000 but it's better to verify it because blender can indicate transparency when
|
||||
// it is not required
|
||||
boolean transparent = false; |
||||
if (diffuseColor != null) { |
||||
transparent = diffuseColor.a < 1.0f; |
||||
if (loadedTextures.size() > 0) {// texture covers the material color
|
||||
diffuseColor.set(1, 1, 1, 1); |
||||
} |
||||
} |
||||
if (specularColor != null) { |
||||
transparent = transparent || specularColor.a < 1.0f; |
||||
} |
||||
this.transparent = transparent; |
||||
} else { |
||||
transparent = false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return the name of the material |
||||
*/ |
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
/** |
||||
* Applies material to a given geometry. |
||||
* |
||||
* @param geometry |
||||
* the geometry |
||||
* @param geometriesOMA |
||||
* the geometries OMA |
||||
* @param userDefinedUVCoordinates |
||||
* UV coords defined by user |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public void applyMaterial(Geometry geometry, Long geometriesOMA, Map<String, List<Vector2f>> userDefinedUVCoordinates, BlenderContext blenderContext) { |
||||
Material material = null; |
||||
if (shadeless) { |
||||
material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); |
||||
|
||||
if (!transparent) { |
||||
diffuseColor.a = 1; |
||||
} |
||||
|
||||
material.setColor("Color", diffuseColor); |
||||
} else { |
||||
material = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Light/Lighting.j3md"); |
||||
material.setBoolean("UseMaterialColors", Boolean.TRUE); |
||||
|
||||
// setting the colors
|
||||
if (!transparent) { |
||||
diffuseColor.a = 1; |
||||
} |
||||
material.setColor("Diffuse", diffuseColor); |
||||
|
||||
material.setColor("Specular", specularColor); |
||||
material.setFloat("Shininess", shininess); |
||||
|
||||
material.setColor("Ambient", new ColorRGBA(ambientFactor, ambientFactor, ambientFactor, 1f)); |
||||
} |
||||
|
||||
// initializing unused "user-defined UV coords" to all available
|
||||
Map<String, List<Vector2f>> unusedUserDefinedUVCoords = Collections.emptyMap(); |
||||
if(userDefinedUVCoordinates != null && !userDefinedUVCoordinates.isEmpty()) { |
||||
unusedUserDefinedUVCoords = new HashMap<>(userDefinedUVCoordinates); |
||||
} |
||||
|
||||
// applying textures
|
||||
int textureIndex = 0; |
||||
if (loadedTextures != null && loadedTextures.size() > 0) { |
||||
if (loadedTextures.size() > TextureHelper.TEXCOORD_TYPES.length) { |
||||
LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different textures. JME supports only {0} UV mappings.", TextureHelper.TEXCOORD_TYPES.length); |
||||
} |
||||
for (CombinedTexture combinedTexture : loadedTextures) { |
||||
if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) { |
||||
String usedUserUVSet = combinedTexture.flatten(geometry, geometriesOMA, userDefinedUVCoordinates, blenderContext); |
||||
|
||||
this.setTexture(material, combinedTexture.getMappingType(), combinedTexture.getResultTexture()); |
||||
|
||||
if(usedUserUVSet == null || unusedUserDefinedUVCoords.containsKey(usedUserUVSet)) { |
||||
List<Vector2f> uvs = combinedTexture.getResultUVS(); |
||||
if(uvs != null && uvs.size() > 0) { |
||||
VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]); |
||||
uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()]))); |
||||
geometry.getMesh().setBuffer(uvCoordsBuffer); |
||||
}//uvs might be null if the user assigned non existing UV coordinates group name to the mesh (this should be fixed in blender file)
|
||||
|
||||
// Remove used "user-defined UV coords" from the unused collection
|
||||
if(usedUserUVSet != null) { |
||||
unusedUserDefinedUVCoords.remove(usedUserUVSet); |
||||
} |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "The texture could not be applied because JME only supports up to {0} different UV's.", TextureHelper.TEXCOORD_TYPES.length); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (unusedUserDefinedUVCoords != null && unusedUserDefinedUVCoords.size() > 0) { |
||||
LOGGER.fine("Storing unused, user defined UV coordinates sets."); |
||||
if (unusedUserDefinedUVCoords.size() > TextureHelper.TEXCOORD_TYPES.length) { |
||||
LOGGER.log(Level.WARNING, "The blender file has defined more than {0} different UV coordinates for the mesh. JME supports only {0} UV coordinates buffers.", TextureHelper.TEXCOORD_TYPES.length); |
||||
} |
||||
for (Entry<String, List<Vector2f>> entry : unusedUserDefinedUVCoords.entrySet()) { |
||||
if (textureIndex < TextureHelper.TEXCOORD_TYPES.length) { |
||||
List<Vector2f> uvs = entry.getValue(); |
||||
VertexBuffer uvCoordsBuffer = new VertexBuffer(TextureHelper.TEXCOORD_TYPES[textureIndex++]); |
||||
uvCoordsBuffer.setupData(Usage.Static, 2, Format.Float, BufferUtils.createFloatBuffer(uvs.toArray(new Vector2f[uvs.size()]))); |
||||
geometry.getMesh().setBuffer(uvCoordsBuffer); |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "The user's UV set named: '{0}' could not be stored because JME only supports up to {1} different UV's.", new Object[] { |
||||
entry.getKey(), TextureHelper.TEXCOORD_TYPES.length |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// applying additional data
|
||||
material.setName(name); |
||||
if (vertexColor) { |
||||
material.setBoolean(shadeless ? "VertexColor" : "UseVertexColor", true); |
||||
} |
||||
material.getAdditionalRenderState().setFaceCullMode(faceCullMode != null ? faceCullMode : blenderContext.getBlenderKey().getFaceCullMode()); |
||||
if (transparent) { |
||||
material.setTransparent(true); |
||||
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); |
||||
geometry.setQueueBucket(Bucket.Transparent); |
||||
} |
||||
|
||||
geometry.setMaterial(material); |
||||
} |
||||
|
||||
/** |
||||
* Sets the texture to the given material. |
||||
* |
||||
* @param material |
||||
* the material that we add texture to |
||||
* @param mapTo |
||||
* the texture mapping type |
||||
* @param texture |
||||
* the added texture |
||||
*/ |
||||
private void setTexture(Material material, int mapTo, Texture texture) { |
||||
switch (mapTo) { |
||||
case MTEX_COL: |
||||
material.setTexture(shadeless ? MaterialHelper.TEXTURE_TYPE_COLOR : MaterialHelper.TEXTURE_TYPE_DIFFUSE, texture); |
||||
break; |
||||
case MTEX_NOR: |
||||
material.setTexture(MaterialHelper.TEXTURE_TYPE_NORMAL, texture); |
||||
break; |
||||
case MTEX_SPEC: |
||||
material.setTexture(MaterialHelper.TEXTURE_TYPE_SPECULAR, texture); |
||||
break; |
||||
case MTEX_EMIT: |
||||
material.setTexture(MaterialHelper.TEXTURE_TYPE_GLOW, texture); |
||||
break; |
||||
case MTEX_ALPHA: |
||||
if (!shadeless) { |
||||
material.setTexture(MaterialHelper.TEXTURE_TYPE_ALPHA, texture); |
||||
} else { |
||||
LOGGER.warning("JME does not support alpha map on unshaded material. Material name is " + name); |
||||
} |
||||
break; |
||||
case MTEX_AMB: |
||||
material.setTexture(MaterialHelper.TEXTURE_TYPE_LIGHTMAP, texture); |
||||
break; |
||||
default: |
||||
LOGGER.severe("Unknown mapping type: " + mapTo); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the material has at least one generated texture and <b>false</b> otherwise |
||||
*/ |
||||
public boolean hasGeneratedTextures() { |
||||
if (loadedTextures != null) { |
||||
for (CombinedTexture generatedTextures : loadedTextures) { |
||||
if (generatedTextures.hasGeneratedTextures()) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the face cull mode. |
||||
* @param faceCullMode |
||||
* the face cull mode |
||||
*/ |
||||
public void setFaceCullMode(FaceCullMode faceCullMode) { |
||||
this.faceCullMode = faceCullMode; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the diffuse color. |
||||
* |
||||
* @param materialStructure |
||||
* the material structure |
||||
* @param diffuseShader |
||||
* the diffuse shader |
||||
* @return the diffuse color |
||||
*/ |
||||
private ColorRGBA readDiffuseColor(Structure materialStructure, DiffuseShader diffuseShader) { |
||||
// bitwise 'or' of all textures mappings
|
||||
int commonMapto = ((Number) materialStructure.getFieldValue("mapto")).intValue(); |
||||
|
||||
// diffuse color
|
||||
float r = ((Number) materialStructure.getFieldValue("r")).floatValue(); |
||||
float g = ((Number) materialStructure.getFieldValue("g")).floatValue(); |
||||
float b = ((Number) materialStructure.getFieldValue("b")).floatValue(); |
||||
float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue(); |
||||
if ((commonMapto & 0x01) == 0x01) {// Col
|
||||
return new ColorRGBA(r, g, b, alpha); |
||||
} else { |
||||
switch (diffuseShader) { |
||||
case FRESNEL: |
||||
case ORENNAYAR: |
||||
case TOON: |
||||
break;// TODO: find what is the proper modification
|
||||
case MINNAERT: |
||||
case LAMBERT:// TODO: check if that is correct
|
||||
float ref = ((Number) materialStructure.getFieldValue("ref")).floatValue(); |
||||
r *= ref; |
||||
g *= ref; |
||||
b *= ref; |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown diffuse shader type: " + diffuseShader.toString()); |
||||
} |
||||
return new ColorRGBA(r, g, b, alpha); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns a specular color used by the material. |
||||
* |
||||
* @param materialStructure |
||||
* the material structure filled with data |
||||
* @return a specular color used by the material |
||||
*/ |
||||
private ColorRGBA readSpecularColor(Structure materialStructure) { |
||||
float specularIntensity = ((Number) materialStructure.getFieldValue("spec")).floatValue(); |
||||
float r = ((Number) materialStructure.getFieldValue("specr")).floatValue() * specularIntensity; |
||||
float g = ((Number) materialStructure.getFieldValue("specg")).floatValue() * specularIntensity; |
||||
float b = ((Number) materialStructure.getFieldValue("specb")).floatValue() * specularIntensity; |
||||
float alpha = ((Number) materialStructure.getFieldValue("alpha")).floatValue(); |
||||
return new ColorRGBA(r, g, b, alpha); |
||||
} |
||||
|
||||
@Override |
||||
public void write(JmeExporter e) throws IOException { |
||||
throw new IOException("Material context is not for saving! It implements savable only to be passed to another blend file as a Savable in user data!"); |
||||
} |
||||
|
||||
@Override |
||||
public void read(JmeImporter e) throws IOException { |
||||
throw new IOException("Material context is not for loading! It implements savable only to be passed to another blend file as a Savable in user data!"); |
||||
} |
||||
} |
@ -0,0 +1,387 @@ |
||||
/* |
||||
* Copyright (c) 2009-2020 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.materials; |
||||
|
||||
import java.nio.ByteBuffer; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.material.MatParam; |
||||
import com.jme3.material.Material; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.shader.VarType; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Image.Format; |
||||
import com.jme3.texture.image.ColorSpace; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
public class MaterialHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(MaterialHelper.class.getName()); |
||||
protected static final float DEFAULT_SHININESS = 20.0f; |
||||
|
||||
public static final String TEXTURE_TYPE_COLOR = "ColorMap"; |
||||
public static final String TEXTURE_TYPE_DIFFUSE = "DiffuseMap"; |
||||
public static final String TEXTURE_TYPE_NORMAL = "NormalMap"; |
||||
public static final String TEXTURE_TYPE_SPECULAR = "SpecularMap"; |
||||
public static final String TEXTURE_TYPE_GLOW = "GlowMap"; |
||||
public static final String TEXTURE_TYPE_ALPHA = "AlphaMap"; |
||||
public static final String TEXTURE_TYPE_LIGHTMAP = "LightMap"; |
||||
|
||||
public static final Integer ALPHA_MASK_NONE = Integer.valueOf(0); |
||||
public static final Integer ALPHA_MASK_CIRCLE = Integer.valueOf(1); |
||||
public static final Integer ALPHA_MASK_CONE = Integer.valueOf(2); |
||||
public static final Integer ALPHA_MASK_HYPERBOLE = Integer.valueOf(3); |
||||
protected final Map<Integer, IAlphaMask> alphaMasks = new HashMap<Integer, IAlphaMask>(); |
||||
|
||||
/** |
||||
* The type of the material's diffuse shader. |
||||
*/ |
||||
public static enum DiffuseShader { |
||||
LAMBERT, ORENNAYAR, TOON, MINNAERT, FRESNEL |
||||
} |
||||
|
||||
/** |
||||
* The type of the material's specular shader. |
||||
*/ |
||||
public static enum SpecularShader { |
||||
COOKTORRENCE, PHONG, BLINN, TOON, WARDISO |
||||
} |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender |
||||
* versions. |
||||
* |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public MaterialHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
// setting alpha masks
|
||||
alphaMasks.put(ALPHA_MASK_NONE, new IAlphaMask() { |
||||
@Override |
||||
public void setImageSize(int width, int height) { |
||||
} |
||||
|
||||
@Override |
||||
public byte getAlpha(float x, float y) { |
||||
return (byte) 255; |
||||
} |
||||
}); |
||||
alphaMasks.put(ALPHA_MASK_CIRCLE, new IAlphaMask() { |
||||
private float r; |
||||
private float[] center; |
||||
|
||||
@Override |
||||
public void setImageSize(int width, int height) { |
||||
r = Math.min(width, height) * 0.5f; |
||||
center = new float[] { width * 0.5f, height * 0.5f }; |
||||
} |
||||
|
||||
@Override |
||||
public byte getAlpha(float x, float y) { |
||||
float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1]))); |
||||
return (byte) (d >= r ? 0 : 255); |
||||
} |
||||
}); |
||||
alphaMasks.put(ALPHA_MASK_CONE, new IAlphaMask() { |
||||
private float r; |
||||
private float[] center; |
||||
|
||||
@Override |
||||
public void setImageSize(int width, int height) { |
||||
r = Math.min(width, height) * 0.5f; |
||||
center = new float[] { width * 0.5f, height * 0.5f }; |
||||
} |
||||
|
||||
@Override |
||||
public byte getAlpha(float x, float y) { |
||||
float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1]))); |
||||
return (byte) (d >= r ? 0 : -255.0f * d / r + 255.0f); |
||||
} |
||||
}); |
||||
alphaMasks.put(ALPHA_MASK_HYPERBOLE, new IAlphaMask() { |
||||
private float r; |
||||
private float[] center; |
||||
|
||||
@Override |
||||
public void setImageSize(int width, int height) { |
||||
r = Math.min(width, height) * 0.5f; |
||||
center = new float[] { width * 0.5f, height * 0.5f }; |
||||
} |
||||
|
||||
@Override |
||||
public byte getAlpha(float x, float y) { |
||||
float d = FastMath.abs(FastMath.sqrt((x - center[0]) * (x - center[0]) + (y - center[1]) * (y - center[1]))) / r; |
||||
return d >= 1.0f ? 0 : (byte) ((-FastMath.sqrt((2.0f - d) * d) + 1.0f) * 255.0f); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* This method converts the material structure to jme Material. |
||||
* @param structure |
||||
* structure with material data |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return jme material |
||||
* @throws BlenderFileException |
||||
* an exception is throw when problems with blend file occur |
||||
*/ |
||||
public MaterialContext toMaterialContext(Structure structure, BlenderContext blenderContext) throws BlenderFileException { |
||||
MaterialContext result = (MaterialContext) blenderContext.getLoadedFeature(structure.getOldMemoryAddress(), LoadedDataType.FEATURE); |
||||
if (result != null) { |
||||
return result; |
||||
} |
||||
|
||||
if ("ID".equals(structure.getType())) { |
||||
LOGGER.fine("Loading material from external blend file."); |
||||
return (MaterialContext) this.loadLibrary(structure); |
||||
} |
||||
|
||||
LOGGER.fine("Loading material."); |
||||
result = new MaterialContext(structure, blenderContext); |
||||
LOGGER.log(Level.FINE, "Material''s name: {0}", result.name); |
||||
Long oma = structure.getOldMemoryAddress(); |
||||
blenderContext.addLoadedFeatures(oma, LoadedDataType.STRUCTURE, structure); |
||||
blenderContext.addLoadedFeatures(oma, LoadedDataType.FEATURE, result); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given material into particles-usable material. |
||||
* The texture and glow color are being copied. |
||||
* The method assumes it receives the Lighting type of material. |
||||
* @param material |
||||
* the source material |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return material converted into particles-usable material |
||||
*/ |
||||
public Material getParticlesMaterial(Material material, Integer alphaMaskIndex, BlenderContext blenderContext) { |
||||
Material result = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); |
||||
|
||||
// copying texture
|
||||
MatParam diffuseMap = material.getParam("DiffuseMap"); |
||||
if (diffuseMap != null) { |
||||
Texture texture = ((Texture) diffuseMap.getValue()).clone(); |
||||
|
||||
// applying alpha mask to the texture
|
||||
Image image = texture.getImage(); |
||||
ByteBuffer sourceBB = image.getData(0); |
||||
sourceBB.rewind(); |
||||
int w = image.getWidth(); |
||||
int h = image.getHeight(); |
||||
ByteBuffer bb = BufferUtils.createByteBuffer(w * h * 4); |
||||
IAlphaMask iAlphaMask = alphaMasks.get(alphaMaskIndex); |
||||
iAlphaMask.setImageSize(w, h); |
||||
|
||||
for (int x = 0; x < w; ++x) { |
||||
for (int y = 0; y < h; ++y) { |
||||
bb.put(sourceBB.get()); |
||||
bb.put(sourceBB.get()); |
||||
bb.put(sourceBB.get()); |
||||
bb.put(iAlphaMask.getAlpha(x, y)); |
||||
} |
||||
} |
||||
|
||||
image = new Image(Format.RGBA8, w, h, bb, ColorSpace.Linear); |
||||
texture.setImage(image); |
||||
|
||||
result.setTextureParam("Texture", VarType.Texture2D, texture); |
||||
} |
||||
|
||||
// copying glow color
|
||||
MatParam glowColor = material.getParam("GlowColor"); |
||||
if (glowColor != null) { |
||||
ColorRGBA color = (ColorRGBA) glowColor.getValue(); |
||||
result.setParam("GlowColor", VarType.Vector3, color); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the table of materials connected to the specified structure. The given structure can be of any type (ie. mesh or |
||||
* curve) but needs to have 'mat' field/ |
||||
* |
||||
* @param structureWithMaterials |
||||
* the structure containing the mesh data |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of vertices colors, each color belongs to a single vertex |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is somehow invalid or corrupted |
||||
*/ |
||||
public MaterialContext[] getMaterials(Structure structureWithMaterials, BlenderContext blenderContext) throws BlenderFileException { |
||||
Pointer ppMaterials = (Pointer) structureWithMaterials.getFieldValue("mat"); |
||||
MaterialContext[] materials = null; |
||||
if (ppMaterials.isNotNull()) { |
||||
List<Structure> materialStructures = ppMaterials.fetchData(); |
||||
if (materialStructures != null && materialStructures.size() > 0) { |
||||
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); |
||||
materials = new MaterialContext[materialStructures.size()]; |
||||
int i = 0; |
||||
for (Structure s : materialStructures) { |
||||
materials[i++] = s == null ? null : materialHelper.toMaterialContext(s, blenderContext); |
||||
} |
||||
} |
||||
} |
||||
return materials; |
||||
} |
||||
|
||||
/** |
||||
* This method converts rgb values to hsv values. |
||||
* |
||||
* @param r |
||||
* red value of the color |
||||
* @param g |
||||
* green value of the color |
||||
* @param b |
||||
* blue value of the color |
||||
* @param hsv |
||||
* hsv values of a color (this table contains the result of the transformation) |
||||
*/ |
||||
public void rgbToHsv(float r, float g, float b, float[] hsv) { |
||||
float cmax = r; |
||||
float cmin = r; |
||||
cmax = g > cmax ? g : cmax; |
||||
cmin = g < cmin ? g : cmin; |
||||
cmax = b > cmax ? b : cmax; |
||||
cmin = b < cmin ? b : cmin; |
||||
|
||||
hsv[2] = cmax; /* value */ |
||||
if (cmax != 0.0) { |
||||
hsv[1] = (cmax - cmin) / cmax; |
||||
} else { |
||||
hsv[1] = 0.0f; |
||||
hsv[0] = 0.0f; |
||||
} |
||||
if (hsv[1] == 0.0) { |
||||
hsv[0] = -1.0f; |
||||
} else { |
||||
float cdelta = cmax - cmin; |
||||
float rc = (cmax - r) / cdelta; |
||||
float gc = (cmax - g) / cdelta; |
||||
float bc = (cmax - b) / cdelta; |
||||
if (r == cmax) { |
||||
hsv[0] = bc - gc; |
||||
} else if (g == cmax) { |
||||
hsv[0] = 2.0f + rc - bc; |
||||
} else { |
||||
hsv[0] = 4.0f + gc - rc; |
||||
} |
||||
hsv[0] *= 60.0f; |
||||
if (hsv[0] < 0.0f) { |
||||
hsv[0] += 360.0f; |
||||
} |
||||
} |
||||
|
||||
hsv[0] /= 360.0f; |
||||
if (hsv[0] < 0.0f) { |
||||
hsv[0] = 0.0f; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method converts rgb values to hsv values. |
||||
* |
||||
* @param h |
||||
* hue |
||||
* @param s |
||||
* saturation |
||||
* @param v |
||||
* value |
||||
* @param rgb |
||||
* rgb result vector (should have 3 elements) |
||||
*/ |
||||
public void hsvToRgb(float h, float s, float v, float[] rgb) { |
||||
h *= 360.0f; |
||||
if (s == 0.0) { |
||||
rgb[0] = rgb[1] = rgb[2] = v; |
||||
} else { |
||||
if (h == 360) { |
||||
h = 0; |
||||
} else { |
||||
h /= 60; |
||||
} |
||||
int i = (int) Math.floor(h); |
||||
float f = h - i; |
||||
float p = v * (1.0f - s); |
||||
float q = v * (1.0f - s * f); |
||||
float t = v * (1.0f - s * (1.0f - f)); |
||||
switch (i) { |
||||
case 0: |
||||
rgb[0] = v; |
||||
rgb[1] = t; |
||||
rgb[2] = p; |
||||
break; |
||||
case 1: |
||||
rgb[0] = q; |
||||
rgb[1] = v; |
||||
rgb[2] = p; |
||||
break; |
||||
case 2: |
||||
rgb[0] = p; |
||||
rgb[1] = v; |
||||
rgb[2] = t; |
||||
break; |
||||
case 3: |
||||
rgb[0] = p; |
||||
rgb[1] = q; |
||||
rgb[2] = v; |
||||
break; |
||||
case 4: |
||||
rgb[0] = t; |
||||
rgb[1] = p; |
||||
rgb[2] = v; |
||||
break; |
||||
case 5: |
||||
rgb[0] = v; |
||||
rgb[1] = p; |
||||
rgb[2] = q; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,583 @@ |
||||
/* |
||||
* Copyright (c) 2009-2020 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.math; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.jme3.export.InputCapsule; |
||||
import com.jme3.export.JmeExporter; |
||||
import com.jme3.export.JmeImporter; |
||||
import com.jme3.export.OutputCapsule; |
||||
import com.jme3.export.Savable; |
||||
import com.jme3.math.Quaternion; |
||||
|
||||
/** |
||||
* <code>DQuaternion</code> defines a single example of a more general class of |
||||
* hypercomplex numbers. DQuaternions extends a rotation in three dimensions to a |
||||
* rotation in four dimensions. This avoids "gimbal lock" and allows for smooth |
||||
* continuous rotation. |
||||
* |
||||
* <code>DQuaternion</code> is defined by four double point numbers: {x y z w}. |
||||
* |
||||
* This class's only purpose is to give better accuracy in floating point operations during computations. |
||||
* This is made by copying the original Quaternion class from jme3 core and leaving only required methods and basic computation methods, so that |
||||
* the class is smaller and easier to maintain. |
||||
* Should any other methods be needed, they will be added. |
||||
* |
||||
* @author Mark Powell |
||||
* @author Joshua Slack |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public final class DQuaternion implements Savable, Cloneable, java.io.Serializable { |
||||
private static final long serialVersionUID = 5009180713885017539L; |
||||
|
||||
/** |
||||
* Represents the identity quaternion rotation (0, 0, 0, 1). |
||||
*/ |
||||
public static final DQuaternion IDENTITY = new DQuaternion(); |
||||
public static final DQuaternion DIRECTION_Z = new DQuaternion(); |
||||
public static final DQuaternion ZERO = new DQuaternion(0, 0, 0, 0); |
||||
protected double x, y, z, w = 1; |
||||
|
||||
/** |
||||
* Constructor instantiates a new <code>DQuaternion</code> object |
||||
* initializing all values to zero, except w which is initialized to 1. |
||||
* |
||||
*/ |
||||
public DQuaternion() { |
||||
} |
||||
|
||||
/** |
||||
* Constructor instantiates a new <code>DQuaternion</code> object from the |
||||
* given list of parameters. |
||||
* |
||||
* @param x |
||||
* the x value of the quaternion. |
||||
* @param y |
||||
* the y value of the quaternion. |
||||
* @param z |
||||
* the z value of the quaternion. |
||||
* @param w |
||||
* the w value of the quaternion. |
||||
*/ |
||||
public DQuaternion(double x, double y, double z, double w) { |
||||
this.set(x, y, z, w); |
||||
} |
||||
|
||||
public DQuaternion(Quaternion q) { |
||||
this(q.getX(), q.getY(), q.getZ(), q.getW()); |
||||
} |
||||
|
||||
public Quaternion toQuaternion() { |
||||
return new Quaternion((float) x, (float) y, (float) z, (float) w); |
||||
} |
||||
|
||||
public double getX() { |
||||
return x; |
||||
} |
||||
|
||||
public double getY() { |
||||
return y; |
||||
} |
||||
|
||||
public double getZ() { |
||||
return z; |
||||
} |
||||
|
||||
public double getW() { |
||||
return w; |
||||
} |
||||
|
||||
/** |
||||
* sets the data in a <code>DQuaternion</code> object from the given list |
||||
* of parameters. |
||||
* |
||||
* @param x |
||||
* the x value of the quaternion. |
||||
* @param y |
||||
* the y value of the quaternion. |
||||
* @param z |
||||
* the z value of the quaternion. |
||||
* @param w |
||||
* the w value of the quaternion. |
||||
* @return this |
||||
*/ |
||||
public DQuaternion set(double x, double y, double z, double w) { |
||||
this.x = x; |
||||
this.y = y; |
||||
this.z = z; |
||||
this.w = w; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the data in this <code>DQuaternion</code> object to be equal to the |
||||
* passed <code>DQuaternion</code> object. The values are copied producing |
||||
* a new object. |
||||
* |
||||
* @param q |
||||
* The DQuaternion to copy values from. |
||||
* @return this |
||||
*/ |
||||
public DQuaternion set(DQuaternion q) { |
||||
x = q.x; |
||||
y = q.y; |
||||
z = q.z; |
||||
w = q.w; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets this DQuaternion to {0, 0, 0, 1}. Same as calling set(0,0,0,1). |
||||
*/ |
||||
public void loadIdentity() { |
||||
x = y = z = 0; |
||||
w = 1; |
||||
} |
||||
|
||||
/** |
||||
* <code>norm</code> returns the norm of this quaternion. This is the dot |
||||
* product of this quaternion with itself. |
||||
* |
||||
* @return the norm of the quaternion. |
||||
*/ |
||||
public double norm() { |
||||
return w * w + x * x + y * y + z * z; |
||||
} |
||||
|
||||
public DQuaternion fromRotationMatrix(double m00, double m01, double m02, |
||||
double m10, double m11, double m12, double m20, double m21, double m22) { |
||||
// first normalize the forward (F), up (U) and side (S) vectors of the rotation matrix
|
||||
// so that the scale does not affect the rotation
|
||||
double lengthSquared = m00 * m00 + m10 * m10 + m20 * m20; |
||||
if (lengthSquared != 1f && lengthSquared != 0f) { |
||||
lengthSquared = 1.0 / Math.sqrt(lengthSquared); |
||||
m00 *= lengthSquared; |
||||
m10 *= lengthSquared; |
||||
m20 *= lengthSquared; |
||||
} |
||||
lengthSquared = m01 * m01 + m11 * m11 + m21 * m21; |
||||
if (lengthSquared != 1 && lengthSquared != 0f) { |
||||
lengthSquared = 1.0 / Math.sqrt(lengthSquared); |
||||
m01 *= lengthSquared; |
||||
m11 *= lengthSquared; |
||||
m21 *= lengthSquared; |
||||
} |
||||
lengthSquared = m02 * m02 + m12 * m12 + m22 * m22; |
||||
if (lengthSquared != 1f && lengthSquared != 0f) { |
||||
lengthSquared = 1.0 / Math.sqrt(lengthSquared); |
||||
m02 *= lengthSquared; |
||||
m12 *= lengthSquared; |
||||
m22 *= lengthSquared; |
||||
} |
||||
|
||||
// Use the Graphics Gems code, from
|
||||
// ftp://ftp.cis.upenn.edu/pub/graphics/shoemake/quatut.ps.Z
|
||||
// *NOT* the "Matrix and Quaternions FAQ", which has errors!
|
||||
|
||||
// the trace is the sum of the diagonal elements; see
|
||||
// http://mathworld.wolfram.com/MatrixTrace.html
|
||||
double t = m00 + m11 + m22; |
||||
|
||||
// we protect the division by s by ensuring that s>=1
|
||||
if (t >= 0) { // |w| >= .5
|
||||
double s = Math.sqrt(t + 1); // |s|>=1 ...
|
||||
w = 0.5f * s; |
||||
s = 0.5f / s; // so this division isn't bad
|
||||
x = (m21 - m12) * s; |
||||
y = (m02 - m20) * s; |
||||
z = (m10 - m01) * s; |
||||
} else if (m00 > m11 && m00 > m22) { |
||||
double s = Math.sqrt(1.0 + m00 - m11 - m22); // |s|>=1
|
||||
x = s * 0.5f; // |x| >= .5
|
||||
s = 0.5f / s; |
||||
y = (m10 + m01) * s; |
||||
z = (m02 + m20) * s; |
||||
w = (m21 - m12) * s; |
||||
} else if (m11 > m22) { |
||||
double s = Math.sqrt(1.0 + m11 - m00 - m22); // |s|>=1
|
||||
y = s * 0.5f; // |y| >= .5
|
||||
s = 0.5f / s; |
||||
x = (m10 + m01) * s; |
||||
z = (m21 + m12) * s; |
||||
w = (m02 - m20) * s; |
||||
} else { |
||||
double s = Math.sqrt(1.0 + m22 - m00 - m11); // |s|>=1
|
||||
z = s * 0.5f; // |z| >= .5
|
||||
s = 0.5f / s; |
||||
x = (m02 + m20) * s; |
||||
y = (m21 + m12) * s; |
||||
w = (m10 - m01) * s; |
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>toRotationMatrix</code> converts this quaternion to a rotational |
||||
* matrix. The result is stored in result. 4th row and 4th column values are |
||||
* untouched. Note: the result is created from a normalized version of this quat. |
||||
* |
||||
* @param result |
||||
* The Matrix4f to store the result in. |
||||
* @return the rotation matrix representation of this quaternion. |
||||
*/ |
||||
public Matrix toRotationMatrix(Matrix result) { |
||||
Vector3d originalScale = new Vector3d(); |
||||
|
||||
result.toScaleVector(originalScale); |
||||
result.setScale(1, 1, 1); |
||||
double norm = this.norm(); |
||||
// we explicitly test norm against one here, saving a division
|
||||
// at the cost of a test and branch. Is it worth it?
|
||||
double s = norm == 1f ? 2f : norm > 0f ? 2f / norm : 0; |
||||
|
||||
// compute xs/ys/zs first to save 6 multiplications, since xs/ys/zs
|
||||
// will be used 2-4 times each.
|
||||
double xs = x * s; |
||||
double ys = y * s; |
||||
double zs = z * s; |
||||
double xx = x * xs; |
||||
double xy = x * ys; |
||||
double xz = x * zs; |
||||
double xw = w * xs; |
||||
double yy = y * ys; |
||||
double yz = y * zs; |
||||
double yw = w * ys; |
||||
double zz = z * zs; |
||||
double zw = w * zs; |
||||
|
||||
// using s=2/norm (instead of 1/norm) saves 9 multiplications by 2 here
|
||||
result.set(0, 0, 1 - (yy + zz)); |
||||
result.set(0, 1, xy - zw); |
||||
result.set(0, 2, xz + yw); |
||||
result.set(1, 0, xy + zw); |
||||
result.set(1, 1, 1 - (xx + zz)); |
||||
result.set(1, 2, yz - xw); |
||||
result.set(2, 0, xz - yw); |
||||
result.set(2, 1, yz + xw); |
||||
result.set(2, 2, 1 - (xx + yy)); |
||||
|
||||
result.setScale(originalScale); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* <code>fromAngleAxis</code> sets this quaternion to the values specified |
||||
* by an angle and an axis of rotation. This method creates an object, so |
||||
* use fromAngleNormalAxis if your axis is already normalized. |
||||
* |
||||
* @param angle |
||||
* the angle to rotate (in radians). |
||||
* @param axis |
||||
* the axis of rotation. |
||||
* @return this quaternion |
||||
*/ |
||||
public DQuaternion fromAngleAxis(double angle, Vector3d axis) { |
||||
Vector3d normAxis = axis.normalize(); |
||||
this.fromAngleNormalAxis(angle, normAxis); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>fromAngleNormalAxis</code> sets this quaternion to the values |
||||
* specified by an angle and a normalized axis of rotation. |
||||
* |
||||
* @param angle |
||||
* the angle to rotate (in radians). |
||||
* @param axis |
||||
* the axis of rotation (already normalized). |
||||
*/ |
||||
public DQuaternion fromAngleNormalAxis(double angle, Vector3d axis) { |
||||
if (axis.x == 0 && axis.y == 0 && axis.z == 0) { |
||||
this.loadIdentity(); |
||||
} else { |
||||
double halfAngle = 0.5f * angle; |
||||
double sin = Math.sin(halfAngle); |
||||
w = Math.cos(halfAngle); |
||||
x = sin * axis.x; |
||||
y = sin * axis.y; |
||||
z = sin * axis.z; |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>add</code> adds the values of this quaternion to those of the |
||||
* parameter quaternion. The result is returned as a new quaternion. |
||||
* |
||||
* @param q |
||||
* the quaternion to add to this. |
||||
* @return the new quaternion. |
||||
*/ |
||||
public DQuaternion add(DQuaternion q) { |
||||
return new DQuaternion(x + q.x, y + q.y, z + q.z, w + q.w); |
||||
} |
||||
|
||||
/** |
||||
* <code>add</code> adds the values of this quaternion to those of the |
||||
* parameter quaternion. The result is stored in this DQuaternion. |
||||
* |
||||
* @param q |
||||
* the quaternion to add to this. |
||||
* @return This DQuaternion after addition. |
||||
*/ |
||||
public DQuaternion addLocal(DQuaternion q) { |
||||
x += q.x; |
||||
y += q.y; |
||||
z += q.z; |
||||
w += q.w; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>subtract</code> subtracts the values of the parameter quaternion |
||||
* from those of this quaternion. The result is returned as a new |
||||
* quaternion. |
||||
* |
||||
* @param q |
||||
* the quaternion to subtract from this. |
||||
* @return the new quaternion. |
||||
*/ |
||||
public DQuaternion subtract(DQuaternion q) { |
||||
return new DQuaternion(x - q.x, y - q.y, z - q.z, w - q.w); |
||||
} |
||||
|
||||
/** |
||||
* <code>subtract</code> subtracts the values of the parameter quaternion |
||||
* from those of this quaternion. The result is stored in this DQuaternion. |
||||
* |
||||
* @param q |
||||
* the quaternion to subtract from this. |
||||
* @return This DQuaternion after subtraction. |
||||
*/ |
||||
public DQuaternion subtractLocal(DQuaternion q) { |
||||
x -= q.x; |
||||
y -= q.y; |
||||
z -= q.z; |
||||
w -= q.w; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>mult</code> multiplies this quaternion by a parameter quaternion. |
||||
* The result is returned as a new quaternion. It should be noted that |
||||
* quaternion multiplication is not commutative so q * p != p * q. |
||||
* |
||||
* @param q |
||||
* the quaternion to multiply this quaternion by. |
||||
* @return the new quaternion. |
||||
*/ |
||||
public DQuaternion mult(DQuaternion q) { |
||||
return this.mult(q, null); |
||||
} |
||||
|
||||
/** |
||||
* <code>mult</code> multiplies this quaternion by a parameter quaternion. |
||||
* The result is returned as a new quaternion. It should be noted that |
||||
* quaternion multiplication is not commutative so q * p != p * q. |
||||
* |
||||
* It IS safe for q and res to be the same object. |
||||
* It IS NOT safe for this and res to be the same object. |
||||
* |
||||
* @param q |
||||
* the quaternion to multiply this quaternion by. |
||||
* @param res |
||||
* the quaternion to store the result in. |
||||
* @return the new quaternion. |
||||
*/ |
||||
public DQuaternion mult(DQuaternion q, DQuaternion res) { |
||||
if (res == null) { |
||||
res = new DQuaternion(); |
||||
} |
||||
double qw = q.w, qx = q.x, qy = q.y, qz = q.z; |
||||
res.x = x * qw + y * qz - z * qy + w * qx; |
||||
res.y = -x * qz + y * qw + z * qx + w * qy; |
||||
res.z = x * qy - y * qx + z * qw + w * qz; |
||||
res.w = -x * qx - y * qy - z * qz + w * qw; |
||||
return res; |
||||
} |
||||
|
||||
/** |
||||
* <code>mult</code> multiplies this quaternion by a parameter vector. The |
||||
* result is returned as a new vector. |
||||
* |
||||
* @param v |
||||
* the vector to multiply this quaternion by. |
||||
* @return the new vector. |
||||
*/ |
||||
public Vector3d mult(Vector3d v) { |
||||
return this.mult(v, null); |
||||
} |
||||
|
||||
/** |
||||
* Multiplies this DQuaternion by the supplied quaternion. The result is |
||||
* stored in this DQuaternion, which is also returned for chaining. Similar |
||||
* to this *= q. |
||||
* |
||||
* @param q |
||||
* The DQuaternion to multiply this one by. |
||||
* @return This DQuaternion, after multiplication. |
||||
*/ |
||||
public DQuaternion multLocal(DQuaternion q) { |
||||
double x1 = x * q.w + y * q.z - z * q.y + w * q.x; |
||||
double y1 = -x * q.z + y * q.w + z * q.x + w * q.y; |
||||
double z1 = x * q.y - y * q.x + z * q.w + w * q.z; |
||||
w = -x * q.x - y * q.y - z * q.z + w * q.w; |
||||
x = x1; |
||||
y = y1; |
||||
z = z1; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>mult</code> multiplies this quaternion by a parameter vector. The |
||||
* result is returned as a new vector. |
||||
* |
||||
* @param v |
||||
* the vector to multiply this quaternion by. |
||||
* @param store |
||||
* the vector to store the result in. It IS safe for v and store |
||||
* to be the same object. |
||||
* @return the result vector. |
||||
*/ |
||||
public Vector3d mult(Vector3d v, Vector3d store) { |
||||
if (store == null) { |
||||
store = new Vector3d(); |
||||
} |
||||
if (v.x == 0 && v.y == 0 && v.z == 0) { |
||||
store.set(0, 0, 0); |
||||
} else { |
||||
double vx = v.x, vy = v.y, vz = v.z; |
||||
store.x = w * w * vx + 2 * y * w * vz - 2 * z * w * vy + x * x * vx + 2 * y * x * vy + 2 * z * x * vz - z * z * vx - y * y * vx; |
||||
store.y = 2 * x * y * vx + y * y * vy + 2 * z * y * vz + 2 * w * z * vx - z * z * vy + w * w * vy - 2 * x * w * vz - x * x * vy; |
||||
store.z = 2 * x * z * vx + 2 * y * z * vy + z * z * vz - 2 * w * y * vx - y * y * vz + 2 * w * x * vy - x * x * vz + w * w * vz; |
||||
} |
||||
return store; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>toString</code> creates the string representation of this <code>DQuaternion</code>. The values of the quaternion are displaced (x, |
||||
* y, z, w), in the following manner: <br> |
||||
* (x, y, z, w) |
||||
* |
||||
* @return the string representation of this object. |
||||
* @see java.lang.Object#toString() |
||||
*/ |
||||
@Override |
||||
public String toString() { |
||||
return "(" + x + ", " + y + ", " + z + ", " + w + ")"; |
||||
} |
||||
|
||||
/** |
||||
* <code>equals</code> determines if two quaternions are logically equal, |
||||
* that is, if the values of (x, y, z, w) are the same for both quaternions. |
||||
* |
||||
* @param o |
||||
* the object to compare for equality |
||||
* @return true if they are equal, false otherwise. |
||||
*/ |
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (!(o instanceof DQuaternion)) { |
||||
return false; |
||||
} |
||||
|
||||
if (this == o) { |
||||
return true; |
||||
} |
||||
|
||||
DQuaternion comp = (DQuaternion) o; |
||||
if (Double.compare(x, comp.x) != 0) { |
||||
return false; |
||||
} |
||||
if (Double.compare(y, comp.y) != 0) { |
||||
return false; |
||||
} |
||||
if (Double.compare(z, comp.z) != 0) { |
||||
return false; |
||||
} |
||||
if (Double.compare(w, comp.w) != 0) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>hashCode</code> returns the hash code value as an integer and is |
||||
* supported for the benefit of hashing based collection classes such as |
||||
* Hashtable, HashMap, HashSet etc. |
||||
* |
||||
* @return the hashcode for this instance of DQuaternion. |
||||
* @see java.lang.Object#hashCode() |
||||
*/ |
||||
@Override |
||||
public int hashCode() { |
||||
long hash = 37; |
||||
hash = 37 * hash + Double.doubleToLongBits(x); |
||||
hash = 37 * hash + Double.doubleToLongBits(y); |
||||
hash = 37 * hash + Double.doubleToLongBits(z); |
||||
hash = 37 * hash + Double.doubleToLongBits(w); |
||||
return (int) hash; |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void write(JmeExporter e) throws IOException { |
||||
OutputCapsule cap = e.getCapsule(this); |
||||
cap.write(x, "x", 0); |
||||
cap.write(y, "y", 0); |
||||
cap.write(z, "z", 0); |
||||
cap.write(w, "w", 1); |
||||
} |
||||
|
||||
@Override |
||||
public void read(JmeImporter e) throws IOException { |
||||
InputCapsule cap = e.getCapsule(this); |
||||
x = cap.readFloat("x", 0); |
||||
y = cap.readFloat("y", 0); |
||||
z = cap.readFloat("z", 0); |
||||
w = cap.readFloat("w", 1); |
||||
} |
||||
|
||||
@Override |
||||
public DQuaternion clone() { |
||||
try { |
||||
return (DQuaternion) super.clone(); |
||||
} catch (CloneNotSupportedException e) { |
||||
throw new AssertionError(); // can not happen
|
||||
} |
||||
} |
||||
} |
@ -0,0 +1,190 @@ |
||||
/* |
||||
* Copyright (c) 2009-2020 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.math; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.jme3.export.InputCapsule; |
||||
import com.jme3.export.JmeExporter; |
||||
import com.jme3.export.JmeImporter; |
||||
import com.jme3.export.OutputCapsule; |
||||
import com.jme3.export.Savable; |
||||
import com.jme3.math.Transform; |
||||
|
||||
/** |
||||
* Started Date: Jul 16, 2004<br> |
||||
* <br> |
||||
* Represents a translation, rotation and scale in one object. |
||||
* |
||||
* This class's only purpose is to give better accuracy in floating point operations during computations. |
||||
* This is made by copying the original Transfrom class from jme3 core and removing unnecessary methods so that |
||||
* the class is smaller and easier to maintain. |
||||
* Should any other methods be needed, they will be added. |
||||
* |
||||
* @author Jack Lindamood |
||||
* @author Joshua Slack |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public final class DTransform implements Savable, Cloneable, java.io.Serializable { |
||||
private static final long serialVersionUID = 7812915425940606722L; |
||||
|
||||
private DQuaternion rotation; |
||||
private Vector3d translation; |
||||
private Vector3d scale; |
||||
|
||||
public DTransform() { |
||||
translation = new Vector3d(); |
||||
rotation = new DQuaternion(); |
||||
scale = new Vector3d(); |
||||
} |
||||
|
||||
public DTransform(Transform transform) { |
||||
translation = new Vector3d(transform.getTranslation()); |
||||
rotation = new DQuaternion(transform.getRotation()); |
||||
scale = new Vector3d(transform.getScale()); |
||||
} |
||||
|
||||
public Transform toTransform() { |
||||
return new Transform(translation.toVector3f(), rotation.toQuaternion(), scale.toVector3f()); |
||||
} |
||||
|
||||
public Matrix toMatrix() { |
||||
Matrix m = Matrix.identity(4); |
||||
m.setTranslation(translation); |
||||
m.setRotationQuaternion(rotation); |
||||
m.setScale(scale); |
||||
return m; |
||||
} |
||||
|
||||
/** |
||||
* Sets this translation to the given value. |
||||
* @param trans |
||||
* The new translation for this matrix. |
||||
* @return this |
||||
*/ |
||||
public DTransform setTranslation(Vector3d trans) { |
||||
translation.set(trans); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets this rotation to the given DQuaternion value. |
||||
* @param rot |
||||
* The new rotation for this matrix. |
||||
* @return this |
||||
*/ |
||||
public DTransform setRotation(DQuaternion rot) { |
||||
rotation.set(rot); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets this scale to the given value. |
||||
* @param scale |
||||
* The new scale for this matrix. |
||||
* @return this |
||||
*/ |
||||
public DTransform setScale(Vector3d scale) { |
||||
this.scale.set(scale); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets this scale to the given value. |
||||
* @param scale |
||||
* The new scale for this matrix. |
||||
* @return this |
||||
*/ |
||||
public DTransform setScale(float scale) { |
||||
this.scale.set(scale, scale, scale); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Return the translation vector in this matrix. |
||||
* @return translation vector. |
||||
*/ |
||||
public Vector3d getTranslation() { |
||||
return translation; |
||||
} |
||||
|
||||
/** |
||||
* Return the rotation quaternion in this matrix. |
||||
* @return rotation quaternion. |
||||
*/ |
||||
public DQuaternion getRotation() { |
||||
return rotation; |
||||
} |
||||
|
||||
/** |
||||
* Return the scale vector in this matrix. |
||||
* @return scale vector. |
||||
*/ |
||||
public Vector3d getScale() { |
||||
return scale; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.getClass().getSimpleName() + "[ " + translation.x + ", " + translation.y + ", " + translation.z + "]\n" + "[ " + rotation.x + ", " + rotation.y + ", " + rotation.z + ", " + rotation.w + "]\n" + "[ " + scale.x + " , " + scale.y + ", " + scale.z + "]"; |
||||
} |
||||
|
||||
@Override |
||||
public void write(JmeExporter e) throws IOException { |
||||
OutputCapsule capsule = e.getCapsule(this); |
||||
capsule.write(rotation, "rot", new DQuaternion()); |
||||
capsule.write(translation, "translation", Vector3d.ZERO); |
||||
capsule.write(scale, "scale", Vector3d.UNIT_XYZ); |
||||
} |
||||
|
||||
@Override |
||||
public void read(JmeImporter e) throws IOException { |
||||
InputCapsule capsule = e.getCapsule(this); |
||||
|
||||
rotation = (DQuaternion) capsule.readSavable("rot", new DQuaternion()); |
||||
translation = (Vector3d) capsule.readSavable("translation", Vector3d.ZERO); |
||||
scale = (Vector3d) capsule.readSavable("scale", Vector3d.UNIT_XYZ); |
||||
} |
||||
|
||||
@Override |
||||
public DTransform clone() { |
||||
try { |
||||
DTransform tq = (DTransform) super.clone(); |
||||
tq.rotation = rotation.clone(); |
||||
tq.scale = scale.clone(); |
||||
tq.translation = translation.clone(); |
||||
return tq; |
||||
} catch (CloneNotSupportedException e) { |
||||
throw new AssertionError(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,210 @@ |
||||
package com.jme3.scene.plugins.blender.math; |
||||
|
||||
import java.text.DecimalFormat; |
||||
|
||||
import org.ejml.ops.CommonOps; |
||||
import org.ejml.simple.SimpleMatrix; |
||||
import org.ejml.simple.SimpleSVD; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
|
||||
/** |
||||
* Encapsulates a 4x4 matrix |
||||
* |
||||
* |
||||
*/ |
||||
public class Matrix extends SimpleMatrix { |
||||
private static final long serialVersionUID = 2396600537315902559L; |
||||
|
||||
public Matrix(int rows, int cols) { |
||||
super(rows, cols); |
||||
} |
||||
|
||||
/** |
||||
* Copy constructor |
||||
*/ |
||||
public Matrix(SimpleMatrix m) { |
||||
super(m); |
||||
} |
||||
|
||||
public Matrix(double[][] data) { |
||||
super(data); |
||||
} |
||||
|
||||
public static Matrix identity(int size) { |
||||
Matrix result = new Matrix(size, size); |
||||
CommonOps.setIdentity(result.mat); |
||||
return result; |
||||
} |
||||
|
||||
public Matrix pseudoinverse() { |
||||
return this.pseudoinverse(1); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
public Matrix pseudoinverse(double lambda) { |
||||
SimpleSVD<SimpleMatrix> simpleSVD = this.svd(); |
||||
|
||||
SimpleMatrix U = simpleSVD.getU(); |
||||
SimpleMatrix S = simpleSVD.getW(); |
||||
SimpleMatrix V = simpleSVD.getV(); |
||||
|
||||
int N = Math.min(this.numRows(),this.numCols()); |
||||
double maxSingular = 0; |
||||
for( int i = 0; i < N; ++i ) { |
||||
if( S.get(i, i) > maxSingular ) { |
||||
maxSingular = S.get(i, i); |
||||
} |
||||
} |
||||
|
||||
double tolerance = FastMath.DBL_EPSILON * Math.max(this.numRows(),this.numCols()) * maxSingular; |
||||
for(int i=0;i<Math.min(S.numRows(), S.numCols());++i) { |
||||
double a = S.get(i, i); |
||||
if(a <= tolerance) { |
||||
a = 0; |
||||
} else { |
||||
a = a/(a * a + lambda * lambda); |
||||
} |
||||
S.set(i, i, a); |
||||
} |
||||
return new Matrix(V.mult(S.transpose()).mult(U.transpose())); |
||||
} |
||||
|
||||
public void setColumn(Vector3d col, int column) { |
||||
this.setColumn(column, 0, col.x, col.y, col.z); |
||||
} |
||||
|
||||
/** |
||||
* Just for some debug informations in order to compare the results with the scilab computation program. |
||||
* @param name the name of the matrix |
||||
* @param m the matrix to print out |
||||
* @return the String format of the matrix to easily input it to Scilab |
||||
*/ |
||||
public String toScilabString(String name, SimpleMatrix m) { |
||||
String result = name + " = ["; |
||||
|
||||
for(int i=0;i<m.numRows();++i) { |
||||
for(int j=0;j<m.numCols();++j) { |
||||
result += m.get(i, j) + " "; |
||||
} |
||||
result += ";"; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return a String representation of the matrix |
||||
*/ |
||||
@Override |
||||
public String toString() { |
||||
DecimalFormat df = new DecimalFormat("#.0000"); |
||||
StringBuilder buf = new StringBuilder(); |
||||
for (int r = 0; r < this.numRows(); ++r) { |
||||
buf.append("\n| "); |
||||
for (int c = 0; c < this.numCols(); ++c) { |
||||
buf.append(df.format(this.get(r, c))).append(' '); |
||||
} |
||||
buf.append('|'); |
||||
} |
||||
return buf.toString(); |
||||
} |
||||
|
||||
public void setTranslation(Vector3d translation) { |
||||
this.setColumn(translation, 3); |
||||
} |
||||
|
||||
/** |
||||
* Sets the scale. |
||||
* |
||||
* @param scale |
||||
* the scale vector to set |
||||
*/ |
||||
public void setScale(Vector3d scale) { |
||||
this.setScale(scale.x, scale.y, scale.z); |
||||
} |
||||
|
||||
/** |
||||
* Sets the scale. |
||||
* |
||||
* @param x |
||||
* the X scale |
||||
* @param y |
||||
* the Y scale |
||||
* @param z |
||||
* the Z scale |
||||
*/ |
||||
public void setScale(double x, double y, double z) { |
||||
Vector3d vect1 = new Vector3d(this.get(0, 0), this.get(1, 0), this.get(2, 0)); |
||||
vect1.normalizeLocal().multLocal(x); |
||||
this.set(0, 0, vect1.x); |
||||
this.set(1, 0, vect1.y); |
||||
this.set(2, 0, vect1.z); |
||||
|
||||
vect1.set(this.get(0, 1), this.get(1, 1), this.get(2, 1)); |
||||
vect1.normalizeLocal().multLocal(y); |
||||
this.set(0, 1, vect1.x); |
||||
this.set(1, 1, vect1.y); |
||||
this.set(2, 1, vect1.z); |
||||
|
||||
vect1.set(this.get(0, 2), this.get(1, 2), this.get(2, 2)); |
||||
vect1.normalizeLocal().multLocal(z); |
||||
this.set(0, 2, vect1.x); |
||||
this.set(1, 2, vect1.y); |
||||
this.set(2, 2, vect1.z); |
||||
} |
||||
|
||||
/** |
||||
* <code>setRotationQuaternion</code> builds a rotation from a |
||||
* <code>Quaternion</code>. |
||||
* |
||||
* @param quat |
||||
* the quaternion to build the rotation from. |
||||
* @throws NullPointerException |
||||
* if quat is null. |
||||
*/ |
||||
public void setRotationQuaternion(DQuaternion quat) { |
||||
quat.toRotationMatrix(this); |
||||
} |
||||
|
||||
public DTransform toTransform() { |
||||
DTransform result = new DTransform(); |
||||
result.setTranslation(this.toTranslationVector()); |
||||
result.setRotation(this.toRotationQuat()); |
||||
result.setScale(this.toScaleVector()); |
||||
return result; |
||||
} |
||||
|
||||
public Vector3d toTranslationVector() { |
||||
return new Vector3d(this.get(0, 3), this.get(1, 3), this.get(2, 3)); |
||||
} |
||||
|
||||
public DQuaternion toRotationQuat() { |
||||
DQuaternion quat = new DQuaternion(); |
||||
quat.fromRotationMatrix(this.get(0, 0), this.get(0, 1), this.get(0, 2), this.get(1, 0), this.get(1, 1), this.get(1, 2), this.get(2, 0), this.get(2, 1), this.get(2, 2)); |
||||
return quat; |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the scale vector from the matrix and stores it into a given |
||||
* vector. |
||||
*/ |
||||
public Vector3d toScaleVector() { |
||||
Vector3d result = new Vector3d(); |
||||
this.toScaleVector(result); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the scale vector from the matrix and stores it into a given |
||||
* vector. |
||||
* |
||||
* @param vector the vector where the scale will be stored |
||||
*/ |
||||
public void toScaleVector(Vector3d vector) { |
||||
double scaleX = Math.sqrt(this.get(0, 0) * this.get(0, 0) + this.get(1, 0) * this.get(1, 0) + this.get(2, 0) * this.get(2, 0)); |
||||
double scaleY = Math.sqrt(this.get(0, 1) * this.get(0, 1) + this.get(1, 1) * this.get(1, 1) + this.get(2, 1) * this.get(2, 1)); |
||||
double scaleZ = Math.sqrt(this.get(0, 2) * this.get(0, 2) + this.get(1, 2) * this.get(1, 2) + this.get(2, 2) * this.get(2, 2)); |
||||
vector.set(scaleX, scaleY, scaleZ); |
||||
} |
||||
} |
@ -0,0 +1,869 @@ |
||||
/* |
||||
* Copyright (c) 2009-2020 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.scene.plugins.blender.math; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.Serializable; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.export.InputCapsule; |
||||
import com.jme3.export.JmeExporter; |
||||
import com.jme3.export.JmeImporter; |
||||
import com.jme3.export.OutputCapsule; |
||||
import com.jme3.export.Savable; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector3f; |
||||
|
||||
/* |
||||
* -- Added *Local methods to cut down on object creation - JS |
||||
*/ |
||||
|
||||
/** |
||||
* <code>Vector3d</code> defines a Vector for a three float value tuple. <code>Vector3d</code> can represent any three dimensional value, such as a |
||||
* vertex, a normal, etc. Utility methods are also included to aid in |
||||
* mathematical calculations. |
||||
* |
||||
* This class's only purpose is to give better accuracy in floating point operations during computations. |
||||
* This is made by copying the original Vector3f class from jme3 core and leaving only required methods and basic computation methods, so that |
||||
* the class is smaller and easier to maintain. |
||||
* Should any other methods be needed, they will be added. |
||||
* |
||||
* @author Mark Powell |
||||
* @author Joshua Slack |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public final class Vector3d implements Savable, Cloneable, Serializable { |
||||
private static final long serialVersionUID = 3090477054277293078L; |
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(Vector3d.class.getName()); |
||||
|
||||
public final static Vector3d ZERO = new Vector3d(); |
||||
public final static Vector3d UNIT_XYZ = new Vector3d(1, 1, 1); |
||||
public final static Vector3d UNIT_X = new Vector3d(1, 0, 0); |
||||
public final static Vector3d UNIT_Y = new Vector3d(0, 1, 0); |
||||
public final static Vector3d UNIT_Z = new Vector3d(0, 0, 1); |
||||
|
||||
/** |
||||
* the x value of the vector. |
||||
*/ |
||||
public double x; |
||||
|
||||
/** |
||||
* the y value of the vector. |
||||
*/ |
||||
public double y; |
||||
|
||||
/** |
||||
* the z value of the vector. |
||||
*/ |
||||
public double z; |
||||
|
||||
/** |
||||
* Constructor instantiates a new <code>Vector3d</code> with default |
||||
* values of (0,0,0). |
||||
* |
||||
*/ |
||||
public Vector3d() { |
||||
} |
||||
|
||||
/** |
||||
* Constructor instantiates a new <code>Vector3d</code> with provides |
||||
* values. |
||||
* |
||||
* @param x |
||||
* the x value of the vector. |
||||
* @param y |
||||
* the y value of the vector. |
||||
* @param z |
||||
* the z value of the vector. |
||||
*/ |
||||
public Vector3d(double x, double y, double z) { |
||||
this.x = x; |
||||
this.y = y; |
||||
this.z = z; |
||||
} |
||||
|
||||
/** |
||||
* Constructor instantiates a new <code>Vector3d</code> that is a copy |
||||
* of the provided vector |
||||
* @param vector3f |
||||
* The Vector3f to copy |
||||
*/ |
||||
public Vector3d(Vector3f vector3f) { |
||||
this(vector3f.x, vector3f.y, vector3f.z); |
||||
} |
||||
|
||||
public Vector3f toVector3f() { |
||||
return new Vector3f((float) x, (float) y, (float) z); |
||||
} |
||||
|
||||
/** |
||||
* <code>set</code> sets the x,y,z values of the vector based on passed |
||||
* parameters. |
||||
* |
||||
* @param x |
||||
* the x value of the vector. |
||||
* @param y |
||||
* the y value of the vector. |
||||
* @param z |
||||
* the z value of the vector. |
||||
* @return this vector |
||||
*/ |
||||
public Vector3d set(double x, double y, double z) { |
||||
this.x = x; |
||||
this.y = y; |
||||
this.z = z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>set</code> sets the x,y,z values of the vector by copying the |
||||
* supplied vector. |
||||
* |
||||
* @param vect |
||||
* the vector to copy. |
||||
* @return this vector |
||||
*/ |
||||
public Vector3d set(Vector3d vect) { |
||||
return this.set(vect.x, vect.y, vect.z); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>add</code> adds a provided vector to this vector creating a |
||||
* resultant vector which is returned. If the provided vector is null, null |
||||
* is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to add to this. |
||||
* @return the resultant vector. |
||||
*/ |
||||
public Vector3d add(Vector3d vec) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, null returned."); |
||||
return null; |
||||
} |
||||
return new Vector3d(x + vec.x, y + vec.y, z + vec.z); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>add</code> adds the values of a provided vector storing the |
||||
* values in the supplied vector. |
||||
* |
||||
* @param vec |
||||
* the vector to add to this |
||||
* @param result |
||||
* the vector to store the result in |
||||
* @return result returns the supplied result vector. |
||||
*/ |
||||
public Vector3d add(Vector3d vec, Vector3d result) { |
||||
result.x = x + vec.x; |
||||
result.y = y + vec.y; |
||||
result.z = z + vec.z; |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* <code>addLocal</code> adds a provided vector to this vector internally, |
||||
* and returns a handle to this vector for easy chaining of calls. If the |
||||
* provided vector is null, null is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to add to this vector. |
||||
* @return this |
||||
*/ |
||||
public Vector3d addLocal(Vector3d vec) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, null returned."); |
||||
return null; |
||||
} |
||||
x += vec.x; |
||||
y += vec.y; |
||||
z += vec.z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>add</code> adds the provided values to this vector, creating a |
||||
* new vector that is then returned. |
||||
* |
||||
* @param addX |
||||
* the x value to add. |
||||
* @param addY |
||||
* the y value to add. |
||||
* @param addZ |
||||
* the z value to add. |
||||
* @return the result vector. |
||||
*/ |
||||
public Vector3d add(double addX, double addY, double addZ) { |
||||
return new Vector3d(x + addX, y + addY, z + addZ); |
||||
} |
||||
|
||||
/** |
||||
* <code>addLocal</code> adds the provided values to this vector |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. |
||||
* |
||||
* @param addX |
||||
* value to add to x |
||||
* @param addY |
||||
* value to add to y |
||||
* @param addZ |
||||
* value to add to z |
||||
* @return this |
||||
*/ |
||||
public Vector3d addLocal(double addX, double addY, double addZ) { |
||||
x += addX; |
||||
y += addY; |
||||
z += addZ; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>scaleAdd</code> multiplies this vector by a scalar then adds the |
||||
* given Vector3d. |
||||
* |
||||
* @param scalar |
||||
* the value to multiply this vector by. |
||||
* @param add |
||||
* the value to add |
||||
*/ |
||||
public Vector3d scaleAdd(double scalar, Vector3d add) { |
||||
x = x * scalar + add.x; |
||||
y = y * scalar + add.y; |
||||
z = z * scalar + add.z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>scaleAdd</code> multiplies the given vector by a scalar then adds |
||||
* the given vector. |
||||
* |
||||
* @param scalar |
||||
* the value to multiply this vector by. |
||||
* @param mult |
||||
* the value to multiply the scalar by |
||||
* @param add |
||||
* the value to add |
||||
*/ |
||||
public Vector3d scaleAdd(double scalar, Vector3d mult, Vector3d add) { |
||||
x = mult.x * scalar + add.x; |
||||
y = mult.y * scalar + add.y; |
||||
z = mult.z * scalar + add.z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>dot</code> calculates the dot product of this vector with a |
||||
* provided vector. If the provided vector is null, 0 is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to dot with this vector. |
||||
* @return the resultant dot product of this vector and a given vector. |
||||
*/ |
||||
public double dot(Vector3d vec) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, 0 returned."); |
||||
return 0; |
||||
} |
||||
return x * vec.x + y * vec.y + z * vec.z; |
||||
} |
||||
|
||||
/** |
||||
* <code>cross</code> calculates the cross product of this vector with a |
||||
* parameter vector v. |
||||
* |
||||
* @param v |
||||
* the vector to take the cross product of with this. |
||||
* @return the cross product vector. |
||||
*/ |
||||
public Vector3d cross(Vector3d v) { |
||||
return this.cross(v, null); |
||||
} |
||||
|
||||
/** |
||||
* <code>cross</code> calculates the cross product of this vector with a |
||||
* parameter vector v. The result is stored in <code>result</code> |
||||
* |
||||
* @param v |
||||
* the vector to take the cross product of with this. |
||||
* @param result |
||||
* the vector to store the cross product result. |
||||
* @return result, after receiving the cross product vector. |
||||
*/ |
||||
public Vector3d cross(Vector3d v, Vector3d result) { |
||||
return this.cross(v.x, v.y, v.z, result); |
||||
} |
||||
|
||||
/** |
||||
* <code>cross</code> calculates the cross product of this vector with a |
||||
* parameter vector v. The result is stored in <code>result</code> |
||||
* |
||||
* @param otherX |
||||
* x component of the vector to take the cross product of with this. |
||||
* @param otherY |
||||
* y component of the vector to take the cross product of with this. |
||||
* @param otherZ |
||||
* z component of the vector to take the cross product of with this. |
||||
* @param result |
||||
* the vector to store the cross product result. |
||||
* @return result, after receiving the cross product vector. |
||||
*/ |
||||
public Vector3d cross(double otherX, double otherY, double otherZ, Vector3d result) { |
||||
if (result == null) { |
||||
result = new Vector3d(); |
||||
} |
||||
double resX = y * otherZ - z * otherY; |
||||
double resY = z * otherX - x * otherZ; |
||||
double resZ = x * otherY - y * otherX; |
||||
result.set(resX, resY, resZ); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* <code>crossLocal</code> calculates the cross product of this vector |
||||
* with a parameter vector v. |
||||
* |
||||
* @param v |
||||
* the vector to take the cross product of with this. |
||||
* @return this. |
||||
*/ |
||||
public Vector3d crossLocal(Vector3d v) { |
||||
return this.crossLocal(v.x, v.y, v.z); |
||||
} |
||||
|
||||
/** |
||||
* <code>crossLocal</code> calculates the cross product of this vector |
||||
* with a parameter vector v. |
||||
* |
||||
* @param otherX |
||||
* x component of the vector to take the cross product of with this. |
||||
* @param otherY |
||||
* y component of the vector to take the cross product of with this. |
||||
* @param otherZ |
||||
* z component of the vector to take the cross product of with this. |
||||
* @return this. |
||||
*/ |
||||
public Vector3d crossLocal(double otherX, double otherY, double otherZ) { |
||||
double tempx = y * otherZ - z * otherY; |
||||
double tempy = z * otherX - x * otherZ; |
||||
z = x * otherY - y * otherX; |
||||
x = tempx; |
||||
y = tempy; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>length</code> calculates the magnitude of this vector. |
||||
* |
||||
* @return the length or magnitude of the vector. |
||||
*/ |
||||
public double length() { |
||||
return Math.sqrt(this.lengthSquared()); |
||||
} |
||||
|
||||
/** |
||||
* <code>lengthSquared</code> calculates the squared value of the |
||||
* magnitude of the vector. |
||||
* |
||||
* @return the magnitude squared of the vector. |
||||
*/ |
||||
public double lengthSquared() { |
||||
return x * x + y * y + z * z; |
||||
} |
||||
|
||||
/** |
||||
* <code>distanceSquared</code> calculates the distance squared between |
||||
* this vector and vector v. |
||||
* |
||||
* @param v |
||||
* the second vector to determine the distance squared. |
||||
* @return the distance squared between the two vectors. |
||||
*/ |
||||
public double distanceSquared(Vector3d v) { |
||||
double dx = x - v.x; |
||||
double dy = y - v.y; |
||||
double dz = z - v.z; |
||||
return dx * dx + dy * dy + dz * dz; |
||||
} |
||||
|
||||
/** |
||||
* <code>distance</code> calculates the distance between this vector and |
||||
* vector v. |
||||
* |
||||
* @param v |
||||
* the second vector to determine the distance. |
||||
* @return the distance between the two vectors. |
||||
*/ |
||||
public double distance(Vector3d v) { |
||||
return Math.sqrt(this.distanceSquared(v)); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>mult</code> multiplies this vector by a scalar. The resultant |
||||
* vector is returned. |
||||
* |
||||
* @param scalar |
||||
* the value to multiply this vector by. |
||||
* @return the new vector. |
||||
*/ |
||||
public Vector3d mult(double scalar) { |
||||
return new Vector3d(x * scalar, y * scalar, z * scalar); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>mult</code> multiplies this vector by a scalar. The resultant |
||||
* vector is supplied as the second parameter and returned. |
||||
* |
||||
* @param scalar |
||||
* the scalar to multiply this vector by. |
||||
* @param product |
||||
* the product to store the result in. |
||||
* @return product |
||||
*/ |
||||
public Vector3d mult(double scalar, Vector3d product) { |
||||
if (null == product) { |
||||
product = new Vector3d(); |
||||
} |
||||
|
||||
product.x = x * scalar; |
||||
product.y = y * scalar; |
||||
product.z = z * scalar; |
||||
return product; |
||||
} |
||||
|
||||
/** |
||||
* <code>multLocal</code> multiplies this vector by a scalar internally, |
||||
* and returns a handle to this vector for easy chaining of calls. |
||||
* |
||||
* @param scalar |
||||
* the value to multiply this vector by. |
||||
* @return this |
||||
*/ |
||||
public Vector3d multLocal(double scalar) { |
||||
x *= scalar; |
||||
y *= scalar; |
||||
z *= scalar; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>multLocal</code> multiplies a provided vector by this vector |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. If the provided vector is null, null is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to multiply by this vector. |
||||
* @return this |
||||
*/ |
||||
public Vector3d multLocal(Vector3d vec) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, null returned."); |
||||
return null; |
||||
} |
||||
x *= vec.x; |
||||
y *= vec.y; |
||||
z *= vec.z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>multLocal</code> multiplies this vector by 3 scalars |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. |
||||
* |
||||
* @param x |
||||
* @param y |
||||
* @param z |
||||
* @return this |
||||
*/ |
||||
public Vector3d multLocal(double x, double y, double z) { |
||||
this.x *= x; |
||||
this.y *= y; |
||||
this.z *= z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>multLocal</code> multiplies a provided vector by this vector |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. If the provided vector is null, null is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to mult to this vector. |
||||
* @return this |
||||
*/ |
||||
public Vector3d mult(Vector3d vec) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, null returned."); |
||||
return null; |
||||
} |
||||
return this.mult(vec, null); |
||||
} |
||||
|
||||
/** |
||||
* <code>multLocal</code> multiplies a provided vector by this vector |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. If the provided vector is null, null is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to mult to this vector. |
||||
* @param store |
||||
* result vector (null to create a new vector) |
||||
* @return this |
||||
*/ |
||||
public Vector3d mult(Vector3d vec, Vector3d store) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, null returned."); |
||||
return null; |
||||
} |
||||
if (store == null) { |
||||
store = new Vector3d(); |
||||
} |
||||
return store.set(x * vec.x, y * vec.y, z * vec.z); |
||||
} |
||||
|
||||
/** |
||||
* <code>divide</code> divides the values of this vector by a scalar and |
||||
* returns the result. The values of this vector remain untouched. |
||||
* |
||||
* @param scalar |
||||
* the value to divide this vectors attributes by. |
||||
* @return the result <code>Vector</code>. |
||||
*/ |
||||
public Vector3d divide(double scalar) { |
||||
scalar = 1f / scalar; |
||||
return new Vector3d(x * scalar, y * scalar, z * scalar); |
||||
} |
||||
|
||||
/** |
||||
* <code>divideLocal</code> divides this vector by a scalar internally, |
||||
* and returns a handle to this vector for easy chaining of calls. Dividing |
||||
* by zero will result in an exception. |
||||
* |
||||
* @param scalar |
||||
* the value to divides this vector by. |
||||
* @return this |
||||
*/ |
||||
public Vector3d divideLocal(double scalar) { |
||||
scalar = 1f / scalar; |
||||
x *= scalar; |
||||
y *= scalar; |
||||
z *= scalar; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>divide</code> divides the values of this vector by a scalar and |
||||
* returns the result. The values of this vector remain untouched. |
||||
* |
||||
* @param scalar |
||||
* the value to divide this vectors attributes by. |
||||
* @return the result <code>Vector</code>. |
||||
*/ |
||||
public Vector3d divide(Vector3d scalar) { |
||||
return new Vector3d(x / scalar.x, y / scalar.y, z / scalar.z); |
||||
} |
||||
|
||||
/** |
||||
* <code>divideLocal</code> divides this vector by a scalar internally, |
||||
* and returns a handle to this vector for easy chaining of calls. Dividing |
||||
* by zero will result in an exception. |
||||
* |
||||
* @param scalar |
||||
* the value to divides this vector by. |
||||
* @return this |
||||
*/ |
||||
public Vector3d divideLocal(Vector3d scalar) { |
||||
x /= scalar.x; |
||||
y /= scalar.y; |
||||
z /= scalar.z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>negate</code> returns the negative of this vector. All values are |
||||
* negated and set to a new vector. |
||||
* |
||||
* @return the negated vector. |
||||
*/ |
||||
public Vector3d negate() { |
||||
return new Vector3d(-x, -y, -z); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>negateLocal</code> negates the internal values of this vector. |
||||
* |
||||
* @return this. |
||||
*/ |
||||
public Vector3d negateLocal() { |
||||
x = -x; |
||||
y = -y; |
||||
z = -z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>subtract</code> subtracts the values of a given vector from those |
||||
* of this vector creating a new vector object. If the provided vector is |
||||
* null, null is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to subtract from this vector. |
||||
* @return the result vector. |
||||
*/ |
||||
public Vector3d subtract(Vector3d vec) { |
||||
return new Vector3d(x - vec.x, y - vec.y, z - vec.z); |
||||
} |
||||
|
||||
/** |
||||
* <code>subtractLocal</code> subtracts a provided vector from this vector |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. If the provided vector is null, null is returned. |
||||
* |
||||
* @param vec |
||||
* the vector to subtract |
||||
* @return this |
||||
*/ |
||||
public Vector3d subtractLocal(Vector3d vec) { |
||||
if (null == vec) { |
||||
LOGGER.warning("Provided vector is null, null returned."); |
||||
return null; |
||||
} |
||||
x -= vec.x; |
||||
y -= vec.y; |
||||
z -= vec.z; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>subtract</code> |
||||
* |
||||
* @param vec |
||||
* the vector to subtract from this |
||||
* @param result |
||||
* the vector to store the result in |
||||
* @return result |
||||
*/ |
||||
public Vector3d subtract(Vector3d vec, Vector3d result) { |
||||
if (result == null) { |
||||
result = new Vector3d(); |
||||
} |
||||
result.x = x - vec.x; |
||||
result.y = y - vec.y; |
||||
result.z = z - vec.z; |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* <code>subtract</code> subtracts the provided values from this vector, |
||||
* creating a new vector that is then returned. |
||||
* |
||||
* @param subtractX |
||||
* the x value to subtract. |
||||
* @param subtractY |
||||
* the y value to subtract. |
||||
* @param subtractZ |
||||
* the z value to subtract. |
||||
* @return the result vector. |
||||
*/ |
||||
public Vector3d subtract(double subtractX, double subtractY, double subtractZ) { |
||||
return new Vector3d(x - subtractX, y - subtractY, z - subtractZ); |
||||
} |
||||
|
||||
/** |
||||
* <code>subtractLocal</code> subtracts the provided values from this vector |
||||
* internally, and returns a handle to this vector for easy chaining of |
||||
* calls. |
||||
* |
||||
* @param subtractX |
||||
* the x value to subtract. |
||||
* @param subtractY |
||||
* the y value to subtract. |
||||
* @param subtractZ |
||||
* the z value to subtract. |
||||
* @return this |
||||
*/ |
||||
public Vector3d subtractLocal(double subtractX, double subtractY, double subtractZ) { |
||||
x -= subtractX; |
||||
y -= subtractY; |
||||
z -= subtractZ; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>normalize</code> returns the unit vector of this vector. |
||||
* |
||||
* @return unit vector of this vector. |
||||
*/ |
||||
public Vector3d normalize() { |
||||
double length = x * x + y * y + z * z; |
||||
if (length != 1f && length != 0f) { |
||||
length = 1.0f / Math.sqrt(length); |
||||
return new Vector3d(x * length, y * length, z * length); |
||||
} |
||||
return this.clone(); |
||||
} |
||||
|
||||
/** |
||||
* <code>normalizeLocal</code> makes this vector into a unit vector of |
||||
* itself. |
||||
* |
||||
* @return this. |
||||
*/ |
||||
public Vector3d normalizeLocal() { |
||||
// NOTE: this implementation is more optimized
|
||||
// than the old jme normalize as this method
|
||||
// is commonly used.
|
||||
double length = x * x + y * y + z * z; |
||||
if (length != 1f && length != 0f) { |
||||
length = 1.0f / Math.sqrt(length); |
||||
x *= length; |
||||
y *= length; |
||||
z *= length; |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* <code>angleBetween</code> returns (in radians) the angle between two vectors. |
||||
* It is assumed that both this vector and the given vector are unit vectors (iow, normalized). |
||||
* |
||||
* @param otherVector |
||||
* a unit vector to find the angle against |
||||
* @return the angle in radians. |
||||
*/ |
||||
public double angleBetween(Vector3d otherVector) { |
||||
double dot = this.dot(otherVector); |
||||
// the vectors are normalized, but if they are parallel then the dot product migh get a value like: 1.000000000000000002
|
||||
// which is caused by floating point operations; in such case, the acos function will return NaN so we need to clamp this value
|
||||
dot = FastMath.clamp((float) dot, -1, 1); |
||||
return Math.acos(dot); |
||||
} |
||||
|
||||
@Override |
||||
public Vector3d clone() { |
||||
try { |
||||
return (Vector3d) super.clone(); |
||||
} catch (CloneNotSupportedException e) { |
||||
throw new AssertionError(); // can not happen
|
||||
} |
||||
} |
||||
|
||||
/** |
||||
* are these two vectors the same? they are is they both have the same x,y, |
||||
* and z values. |
||||
* |
||||
* @param o |
||||
* the object to compare for equality |
||||
* @return true if they are equal |
||||
*/ |
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (!(o instanceof Vector3d)) { |
||||
return false; |
||||
} |
||||
|
||||
if (this == o) { |
||||
return true; |
||||
} |
||||
|
||||
Vector3d comp = (Vector3d) o; |
||||
if (Double.compare(x, comp.x) != 0) { |
||||
return false; |
||||
} |
||||
if (Double.compare(y, comp.y) != 0) { |
||||
return false; |
||||
} |
||||
if (Double.compare(z, comp.z) != 0) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* <code>hashCode</code> returns a unique code for this vector object based |
||||
* on its values. If two vectors are logically equivalent, they will return |
||||
* the same hash code value. |
||||
* @return the hash code value of this vector. |
||||
*/ |
||||
@Override |
||||
public int hashCode() { |
||||
long hash = 37; |
||||
hash += 37 * hash + Double.doubleToLongBits(x); |
||||
hash += 37 * hash + Double.doubleToLongBits(y); |
||||
hash += 37 * hash + Double.doubleToLongBits(z); |
||||
return (int) hash; |
||||
} |
||||
|
||||
/** |
||||
* <code>toString</code> returns the string representation of this vector. |
||||
* The format is: |
||||
* |
||||
* org.jme.math.Vector3d [X=XX.XXXX, Y=YY.YYYY, Z=ZZ.ZZZZ] |
||||
* |
||||
* @return the string representation of this vector. |
||||
*/ |
||||
@Override |
||||
public String toString() { |
||||
return "(" + x + ", " + y + ", " + z + ")"; |
||||
} |
||||
|
||||
@Override |
||||
public void write(JmeExporter e) throws IOException { |
||||
OutputCapsule capsule = e.getCapsule(this); |
||||
capsule.write(x, "x", 0); |
||||
capsule.write(y, "y", 0); |
||||
capsule.write(z, "z", 0); |
||||
} |
||||
|
||||
@Override |
||||
public void read(JmeImporter e) throws IOException { |
||||
InputCapsule capsule = e.getCapsule(this); |
||||
x = capsule.readDouble("x", 0); |
||||
y = capsule.readDouble("y", 0); |
||||
z = capsule.readDouble("z", 0); |
||||
} |
||||
} |
@ -0,0 +1,349 @@ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.math.Vector3d; |
||||
import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate; |
||||
|
||||
/** |
||||
* A class that represents a single edge between two vertices. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class Edge { |
||||
private static final Logger LOGGER = Logger.getLogger(Edge.class.getName()); |
||||
|
||||
private static final int FLAG_EDGE_NOT_IN_FACE = 0x80; |
||||
|
||||
/** The vertices indexes. */ |
||||
private int index1, index2; |
||||
/** The vertices that can be set if we need and abstract edge outside the mesh (for computations). */ |
||||
private Vector3f v1, v2; |
||||
/** The weight of the edge. */ |
||||
private float crease; |
||||
/** A variable that indicates if this edge belongs to any face or not. */ |
||||
private boolean inFace; |
||||
/** The mesh that owns the edge. */ |
||||
private TemporalMesh temporalMesh; |
||||
|
||||
public Edge(Vector3f v1, Vector3f v2) { |
||||
this.v1 = v1 == null ? new Vector3f() : v1; |
||||
this.v2 = v2 == null ? new Vector3f() : v2; |
||||
index1 = 0; |
||||
index2 = 1; |
||||
} |
||||
|
||||
/** |
||||
* This constructor only stores the indexes of the vertices. The position vertices should be stored |
||||
* outside this class. |
||||
* @param index1 |
||||
* the first index of the edge |
||||
* @param index2 |
||||
* the second index of the edge |
||||
* @param crease |
||||
* the weight of the face |
||||
* @param inFace |
||||
* a variable that indicates if this edge belongs to any face or not |
||||
*/ |
||||
public Edge(int index1, int index2, float crease, boolean inFace, TemporalMesh temporalMesh) { |
||||
this.index1 = index1; |
||||
this.index2 = index2; |
||||
this.crease = crease; |
||||
this.inFace = inFace; |
||||
this.temporalMesh = temporalMesh; |
||||
} |
||||
|
||||
@Override |
||||
public Edge clone() { |
||||
return new Edge(index1, index2, crease, inFace, temporalMesh); |
||||
} |
||||
|
||||
/** |
||||
* @return the first index of the edge |
||||
*/ |
||||
public int getFirstIndex() { |
||||
return index1; |
||||
} |
||||
|
||||
/** |
||||
* @return the second index of the edge |
||||
*/ |
||||
public int getSecondIndex() { |
||||
return index2; |
||||
} |
||||
|
||||
/** |
||||
* @return the first vertex of the edge |
||||
*/ |
||||
public Vector3f getFirstVertex() { |
||||
return temporalMesh == null ? v1 : temporalMesh.getVertices().get(index1); |
||||
} |
||||
|
||||
/** |
||||
* @return the second vertex of the edge |
||||
*/ |
||||
public Vector3f getSecondVertex() { |
||||
return temporalMesh == null ? v2 : temporalMesh.getVertices().get(index2); |
||||
} |
||||
|
||||
/** |
||||
* Returns the index other than the given. |
||||
* @param index |
||||
* index of the edge |
||||
* @return the remaining index number |
||||
*/ |
||||
public int getOtherIndex(int index) { |
||||
if (index == index1) { |
||||
return index2; |
||||
} |
||||
if (index == index2) { |
||||
return index1; |
||||
} |
||||
throw new IllegalArgumentException("Cannot give the other index for [" + index + "] because this index does not exist in edge: " + this); |
||||
} |
||||
|
||||
/** |
||||
* @return the crease value of the edge (its weight) |
||||
*/ |
||||
public float getCrease() { |
||||
return crease; |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the edge is used by at least one face and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isInFace() { |
||||
return inFace; |
||||
} |
||||
|
||||
/** |
||||
* @return the length of the edge |
||||
*/ |
||||
public float getLength() { |
||||
return this.getFirstVertex().distance(this.getSecondVertex()); |
||||
} |
||||
|
||||
/** |
||||
* @return the mesh this edge belongs to |
||||
*/ |
||||
public TemporalMesh getTemporalMesh() { |
||||
return temporalMesh; |
||||
} |
||||
|
||||
/** |
||||
* @return the centroid of the edge |
||||
*/ |
||||
public Vector3f computeCentroid() { |
||||
return this.getFirstVertex().add(this.getSecondVertex()).divideLocal(2); |
||||
} |
||||
|
||||
/** |
||||
* Shifts indexes by a given amount. |
||||
* @param shift |
||||
* how much the indexes should be shifted |
||||
* @param predicate |
||||
* the predicate that verifies which indexes should be shifted; if null then all will be shifted |
||||
*/ |
||||
public void shiftIndexes(int shift, IndexPredicate predicate) { |
||||
if (predicate == null) { |
||||
index1 += shift; |
||||
index2 += shift; |
||||
} else { |
||||
index1 += predicate.execute(index1) ? shift : 0; |
||||
index2 += predicate.execute(index2) ? shift : 0; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Flips the order of the indexes. |
||||
*/ |
||||
public void flipIndexes() { |
||||
int temp = index1; |
||||
index1 = index2; |
||||
index2 = temp; |
||||
} |
||||
|
||||
/** |
||||
* The crossing method first computes the points on both lines (that contain the edges) |
||||
* who are closest in distance. If the distance between points is smaller than FastMath.FLT_EPSILON |
||||
* the we consider them to be the same point (the lines cross). |
||||
* The second step is to check if both points are contained within the edges. |
||||
* |
||||
* The method of computing the crossing point is as follows: |
||||
* Let's assume that: |
||||
* (P0, P1) are the points of the first edge |
||||
* (Q0, Q1) are the points of the second edge |
||||
* |
||||
* u = P1 - P0 |
||||
* v = Q1 - Q0 |
||||
* |
||||
* This gives us the equations of two lines: |
||||
* L1: (x = P1x + ux*t1; y = P1y + uy*t1; z = P1z + uz*t1) |
||||
* L2: (x = P2x + vx*t2; y = P2y + vy*t2; z = P2z + vz*t2) |
||||
* |
||||
* Comparing the x and y of the first two equations for each line will allow us to compute t1 and t2 |
||||
* (which is implemented below). |
||||
* Using t1 and t2 we can compute (x, y, z) of each line and that will give us two points that we need to compare. |
||||
* |
||||
* @param edge |
||||
* the edge we check against crossing |
||||
* @return <b>true</b> if the edges cross and false otherwise |
||||
*/ |
||||
public boolean cross(Edge edge) { |
||||
return this.getCrossPoint(edge) != null; |
||||
} |
||||
|
||||
/** |
||||
* The method computes the crossing pint of this edge and another edge. If |
||||
* there is no crossing then null is returned. |
||||
* |
||||
* @param edge |
||||
* the edge to compute corss point with |
||||
* @return cross point on null if none exist |
||||
*/ |
||||
public Vector3f getCrossPoint(Edge edge) { |
||||
return this.getCrossPoint(edge, false, false); |
||||
} |
||||
|
||||
/** |
||||
* The method computes the crossing pint of this edge and another edge. If |
||||
* there is no crossing then null is returned. Also null is returned if the edges are parallel. |
||||
* This method also allows to get the crossing point of the straight lines that contain these edges if |
||||
* you set the 'extend' parameter to true. |
||||
* |
||||
* @param edge |
||||
* the edge to compute corss point with |
||||
* @param extendThisEdge |
||||
* set to <b>true</b> to find a crossing point along the whole |
||||
* straight that contains the current edge |
||||
* @param extendSecondEdge |
||||
* set to <b>true</b> to find a crossing point along the whole |
||||
* straight that contains the given edge |
||||
* @return cross point on null if none exist or the edges are parallel |
||||
*/ |
||||
public Vector3f getCrossPoint(Edge edge, boolean extendThisEdge, boolean extendSecondEdge) { |
||||
Vector3d P1 = new Vector3d(this.getFirstVertex()); |
||||
Vector3d P2 = new Vector3d(edge.getFirstVertex()); |
||||
Vector3d u = new Vector3d(this.getSecondVertex()).subtract(P1).normalizeLocal(); |
||||
Vector3d v = new Vector3d(edge.getSecondVertex()).subtract(P2).normalizeLocal(); |
||||
|
||||
if(Math.abs(u.dot(v)) >= 1 - FastMath.DBL_EPSILON) { |
||||
// the edges are parallel; do not care about the crossing point
|
||||
return null; |
||||
} |
||||
|
||||
double t1 = 0, t2 = 0; |
||||
if(u.x == 0 && v.x == 0) { |
||||
t2 = (u.z * (P2.y - P1.y) - u.y * (P2.z - P1.z)) / (u.y * v.z - u.z * v.y); |
||||
t1 = (P2.z - P1.z + v.z * t2) / u.z; |
||||
} else if(u.y == 0 && v.y == 0) { |
||||
t2 = (u.x * (P2.z - P1.z) - u.z * (P2.x - P1.x)) / (u.z * v.x - u.x * v.z); |
||||
t1 = (P2.x - P1.x + v.x * t2) / u.x; |
||||
} else if(u.z == 0 && v.z == 0) { |
||||
t2 = (u.x * (P2.y - P1.y) - u.y * (P2.x - P1.x)) / (u.y * v.x - u.x * v.y); |
||||
t1 = (P2.x - P1.x + v.x * t2) / u.x; |
||||
} else { |
||||
t2 = (P1.y * u.x - P1.x * u.y + P2.x * u.y - P2.y * u.x) / (v.y * u.x - u.y * v.x); |
||||
t1 = (P2.x - P1.x + v.x * t2) / u.x; |
||||
if(Math.abs(P1.z - P2.z + u.z * t1 - v.z * t2) > FastMath.FLT_EPSILON) { |
||||
return null; |
||||
} |
||||
} |
||||
Vector3d p1 = P1.add(u.mult(t1)); |
||||
Vector3d p2 = P2.add(v.mult(t2)); |
||||
|
||||
if (p1.distance(p2) <= FastMath.FLT_EPSILON) { |
||||
if(extendThisEdge && extendSecondEdge) { |
||||
return p1.toVector3f(); |
||||
} |
||||
// the lines cross, check if p1 and p2 are within the edges
|
||||
Vector3d p = p1.subtract(P1); |
||||
double cos = p.dot(u) / p.length(); |
||||
if (extendThisEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() - this.getLength() <= FastMath.FLT_EPSILON) { |
||||
// p1 is inside the first edge, lets check the other edge now
|
||||
p = p2.subtract(P2); |
||||
cos = p.dot(v) / p.length(); |
||||
if(extendSecondEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() - edge.getLength() <= FastMath.FLT_EPSILON) { |
||||
return p1.toVector3f(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
String result = "Edge [" + index1 + ", " + index2 + "] {" + crease + "}"; |
||||
result += " (" + this.getFirstVertex() + " -> " + this.getSecondVertex() + ")"; |
||||
if (inFace) { |
||||
result += "[F]"; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
// The hash code must be identical for the same two indexes, no matter their order.
|
||||
final int prime = 31; |
||||
int result = 1; |
||||
int lowerIndex = Math.min(index1, index2); |
||||
int higherIndex = Math.max(index1, index2); |
||||
result = prime * result + lowerIndex; |
||||
result = prime * result + higherIndex; |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (!(obj instanceof Edge)) { |
||||
return false; |
||||
} |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
Edge other = (Edge) obj; |
||||
return Math.min(index1, index2) == Math.min(other.index1, other.index2) && Math.max(index1, index2) == Math.max(other.index1, other.index2); |
||||
} |
||||
|
||||
/** |
||||
* The method loads all edges from the given mesh structure that does not belong to any face. |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @param temporalMesh |
||||
* the owner of the edges |
||||
* @return all edges without faces |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with file reading occur |
||||
*/ |
||||
public static List<Edge> loadAll(Structure meshStructure, TemporalMesh temporalMesh) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading all edges that do not belong to any face from mesh: {0}", meshStructure.getName()); |
||||
List<Edge> result = new ArrayList<Edge>(); |
||||
|
||||
Pointer pMEdge = (Pointer) meshStructure.getFieldValue("medge"); |
||||
|
||||
if (pMEdge.isNotNull()) { |
||||
List<Structure> edges = pMEdge.fetchData(); |
||||
for (Structure edge : edges) { |
||||
int flag = ((Number) edge.getFieldValue("flag")).intValue(); |
||||
|
||||
int v1 = ((Number) edge.getFieldValue("v1")).intValue(); |
||||
int v2 = ((Number) edge.getFieldValue("v2")).intValue(); |
||||
// I do not know why, but blender stores (possibly only sometimes) crease as negative values and shows positive in the editor
|
||||
float crease = Math.abs(((Number) edge.getFieldValue("crease")).floatValue()); |
||||
boolean edgeInFace = (flag & Edge.FLAG_EDGE_NOT_IN_FACE) == 0; |
||||
result.add(new Edge(v1, v2, crease, edgeInFace, temporalMesh)); |
||||
} |
||||
} |
||||
LOGGER.log(Level.FINE, "Loaded {0} edges.", result.size()); |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,613 @@ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A class that represents a single face in the mesh. The face is a polygon. Its minimum count of |
||||
* vertices is = 3. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class Face implements Comparator<Integer> { |
||||
private static final Logger LOGGER = Logger.getLogger(Face.class.getName()); |
||||
|
||||
/** The indexes loop of the face. */ |
||||
private IndexesLoop indexes; |
||||
|
||||
private List<IndexesLoop> triangulatedFaces; |
||||
/** Indicates if the face is smooth or solid. */ |
||||
private boolean smooth; |
||||
/** The material index of the face. */ |
||||
private int materialNumber; |
||||
/** UV coordinate sets attached to the face. The key is the set name and value are the UV coords. */ |
||||
private Map<String, List<Vector2f>> faceUVCoords; |
||||
/** The vertex colors of the face. */ |
||||
private List<byte[]> vertexColors; |
||||
/** The temporal mesh the face belongs to. */ |
||||
private TemporalMesh temporalMesh; |
||||
|
||||
/** |
||||
* Creates a complete face with all available data. |
||||
* @param indexes |
||||
* the indexes of the face (required) |
||||
* @param smooth |
||||
* indicates if the face is smooth or solid |
||||
* @param materialNumber |
||||
* the material index of the face |
||||
* @param faceUVCoords |
||||
* UV coordinate sets of the face (optional) |
||||
* @param vertexColors |
||||
* the vertex colors of the face (optional) |
||||
* @param temporalMesh |
||||
* the temporal mesh the face belongs to (required) |
||||
*/ |
||||
public Face(Integer[] indexes, boolean smooth, int materialNumber, Map<String, List<Vector2f>> faceUVCoords, List<byte[]> vertexColors, TemporalMesh temporalMesh) { |
||||
this.setTemporalMesh(temporalMesh); |
||||
this.indexes = new IndexesLoop(indexes); |
||||
this.smooth = smooth; |
||||
this.materialNumber = materialNumber; |
||||
this.faceUVCoords = faceUVCoords; |
||||
this.temporalMesh = temporalMesh; |
||||
this.vertexColors = vertexColors; |
||||
} |
||||
|
||||
/** |
||||
* Default constructor. Used by the clone method. |
||||
*/ |
||||
private Face() { |
||||
} |
||||
|
||||
@Override |
||||
public Face clone() { |
||||
Face result = new Face(); |
||||
result.indexes = indexes.clone(); |
||||
result.smooth = smooth; |
||||
result.materialNumber = materialNumber; |
||||
if (faceUVCoords != null) { |
||||
result.faceUVCoords = new HashMap<String, List<Vector2f>>(faceUVCoords.size()); |
||||
for (Entry<String, List<Vector2f>> entry : faceUVCoords.entrySet()) { |
||||
List<Vector2f> uvs = new ArrayList<Vector2f>(entry.getValue().size()); |
||||
for (Vector2f v : entry.getValue()) { |
||||
uvs.add(v.clone()); |
||||
} |
||||
result.faceUVCoords.put(entry.getKey(), uvs); |
||||
} |
||||
} |
||||
if (vertexColors != null) { |
||||
result.vertexColors = new ArrayList<byte[]>(vertexColors.size()); |
||||
for (byte[] colors : vertexColors) { |
||||
result.vertexColors.add(colors.clone()); |
||||
} |
||||
} |
||||
result.temporalMesh = temporalMesh; |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Returns the index at the given position in the index loop. If the given position is negative or exceeds |
||||
* the amount of vertices - it is being looped properly so that it always hits an index. |
||||
* For example getIndex(-1) will return the index before the 0 - in this case it will be the last one. |
||||
* @param indexPosition |
||||
* the index position |
||||
* @return index value at the given position |
||||
*/ |
||||
private Integer getIndex(int indexPosition) { |
||||
if (indexPosition >= indexes.size()) { |
||||
indexPosition = indexPosition % indexes.size(); |
||||
} else if (indexPosition < 0) { |
||||
indexPosition = indexes.size() - -indexPosition % indexes.size(); |
||||
} |
||||
return indexes.get(indexPosition); |
||||
} |
||||
|
||||
/** |
||||
* @return the mesh this face belongs to |
||||
*/ |
||||
public TemporalMesh getTemporalMesh() { |
||||
return temporalMesh; |
||||
} |
||||
|
||||
/** |
||||
* @return the original indexes of the face |
||||
*/ |
||||
public IndexesLoop getIndexes() { |
||||
return indexes; |
||||
} |
||||
|
||||
/** |
||||
* @return the centroid of the face |
||||
*/ |
||||
public Vector3f computeCentroid() { |
||||
Vector3f result = new Vector3f(); |
||||
List<Vector3f> vertices = temporalMesh.getVertices(); |
||||
for (Integer index : indexes) { |
||||
result.addLocal(vertices.get(index)); |
||||
} |
||||
return result.divideLocal(indexes.size()); |
||||
} |
||||
|
||||
/** |
||||
* @return current indexes of the face (if it is already triangulated then more than one index group will be in the result list) |
||||
*/ |
||||
public List<List<Integer>> getCurrentIndexes() { |
||||
if (triangulatedFaces == null) { |
||||
return Arrays.asList(indexes.getAll()); |
||||
} |
||||
List<List<Integer>> result = new ArrayList<List<Integer>>(triangulatedFaces.size()); |
||||
for (IndexesLoop loop : triangulatedFaces) { |
||||
result.add(loop.getAll()); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method detaches the triangle from the face. This method keeps the indexes loop normalized - every index |
||||
* has only two neighbours. So if detaching the triangle causes a vertex to have more than two neighbours - it is |
||||
* also detached and returned as a result. |
||||
* The result is an empty list if no such situation happens. |
||||
* @param triangleIndexes |
||||
* the indexes of a triangle to be detached |
||||
* @return a list of faces that need to be detached as well in order to keep them normalized |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when vertices of a face create more than one loop; this is found during path finding |
||||
*/ |
||||
private List<Face> detachTriangle(Integer[] triangleIndexes) throws BlenderFileException { |
||||
LOGGER.fine("Detaching triangle."); |
||||
if (triangleIndexes.length != 3) { |
||||
throw new IllegalArgumentException("Cannot detach triangle with that does not have 3 indexes!"); |
||||
} |
||||
MeshHelper meshHelper = temporalMesh.getBlenderContext().getHelper(MeshHelper.class); |
||||
List<Face> detachedFaces = new ArrayList<Face>(); |
||||
List<Integer> path = new ArrayList<Integer>(indexes.size()); |
||||
|
||||
boolean[] edgeRemoved = new boolean[] { indexes.removeEdge(triangleIndexes[0], triangleIndexes[1]), indexes.removeEdge(triangleIndexes[0], triangleIndexes[2]), indexes.removeEdge(triangleIndexes[1], triangleIndexes[2]) }; |
||||
Integer[][] indexesPairs = new Integer[][] { new Integer[] { triangleIndexes[0], triangleIndexes[1] }, new Integer[] { triangleIndexes[0], triangleIndexes[2] }, new Integer[] { triangleIndexes[1], triangleIndexes[2] } }; |
||||
|
||||
for (int i = 0; i < 3; ++i) { |
||||
if (!edgeRemoved[i]) { |
||||
indexes.findPath(indexesPairs[i][0], indexesPairs[i][1], path); |
||||
if (path.size() == 0) { |
||||
indexes.findPath(indexesPairs[i][1], indexesPairs[i][0], path); |
||||
} |
||||
if (path.size() == 0) { |
||||
throw new IllegalStateException("Triangulation failed. Cannot find path between two indexes. Please apply triangulation in Blender as a workaround."); |
||||
} |
||||
if (detachedFaces.size() == 0 && path.size() < indexes.size()) { |
||||
Integer[] indexesSublist = path.toArray(new Integer[path.size()]); |
||||
detachedFaces.add(new Face(indexesSublist, smooth, materialNumber, meshHelper.selectUVSubset(this, indexesSublist), meshHelper.selectVertexColorSubset(this, indexesSublist), temporalMesh)); |
||||
for (int j = 0; j < path.size() - 1; ++j) { |
||||
indexes.removeEdge(path.get(j), path.get(j + 1)); |
||||
} |
||||
indexes.removeEdge(path.get(path.size() - 1), path.get(0)); |
||||
} else { |
||||
indexes.addEdge(path.get(path.size() - 1), path.get(0)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return detachedFaces; |
||||
} |
||||
|
||||
/** |
||||
* Sets the temporal mesh for the face. The given mesh cannot be null. |
||||
* @param temporalMesh |
||||
* the temporal mesh of the face |
||||
* @throws IllegalArgumentException |
||||
* thrown if given temporal mesh is null |
||||
*/ |
||||
public void setTemporalMesh(TemporalMesh temporalMesh) { |
||||
if (temporalMesh == null) { |
||||
throw new IllegalArgumentException("No temporal mesh for the face given!"); |
||||
} |
||||
this.temporalMesh = temporalMesh; |
||||
} |
||||
|
||||
/** |
||||
* Flips the order of the indexes. |
||||
*/ |
||||
public void flipIndexes() { |
||||
indexes.reverse(); |
||||
if (faceUVCoords != null) { |
||||
for (Entry<String, List<Vector2f>> entry : faceUVCoords.entrySet()) { |
||||
Collections.reverse(entry.getValue()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Flips UV coordinates. |
||||
* @param u |
||||
* indicates if U coords should be flipped |
||||
* @param v |
||||
* indicates if V coords should be flipped |
||||
*/ |
||||
public void flipUV(boolean u, boolean v) { |
||||
if (faceUVCoords != null) { |
||||
for (Entry<String, List<Vector2f>> entry : faceUVCoords.entrySet()) { |
||||
for (Vector2f uv : entry.getValue()) { |
||||
uv.set(u ? 1 - uv.x : uv.x, v ? 1 - uv.y : uv.y); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return the UV sets of the face |
||||
*/ |
||||
public Map<String, List<Vector2f>> getUvSets() { |
||||
return faceUVCoords; |
||||
} |
||||
|
||||
/** |
||||
* @return current vertex count of the face |
||||
*/ |
||||
public int vertexCount() { |
||||
return indexes.size(); |
||||
} |
||||
|
||||
/** |
||||
* The method triangulates the face. |
||||
*/ |
||||
public TriangulationWarning triangulate() { |
||||
LOGGER.fine("Triangulating face."); |
||||
assert indexes.size() >= 3 : "Invalid indexes amount for face. 3 is the required minimum!"; |
||||
triangulatedFaces = new ArrayList<IndexesLoop>(indexes.size() - 2); |
||||
Integer[] indexes = new Integer[3]; |
||||
TriangulationWarning warning = TriangulationWarning.NONE; |
||||
|
||||
try { |
||||
List<Face> facesToTriangulate = new ArrayList<Face>(Arrays.asList(this.clone())); |
||||
while (facesToTriangulate.size() > 0 && warning == TriangulationWarning.NONE) { |
||||
Face face = facesToTriangulate.remove(0); |
||||
// two special cases will improve the computations speed
|
||||
if(face.getIndexes().size() == 3) { |
||||
triangulatedFaces.add(face.getIndexes().clone()); |
||||
} else { |
||||
int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1; |
||||
while (face.vertexCount() > 0) { |
||||
indexes[0] = face.getIndex(0); |
||||
indexes[1] = face.findClosestVertex(indexes[0], -1); |
||||
indexes[2] = face.findClosestVertex(indexes[0], indexes[1]); |
||||
|
||||
LOGGER.finer("Veryfying improper triangulation of the temporal mesh."); |
||||
if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) { |
||||
warning = TriangulationWarning.CLOSEST_VERTS; |
||||
break; |
||||
} |
||||
if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) { |
||||
warning = TriangulationWarning.INFINITE_LOOP; |
||||
break; |
||||
} |
||||
previousIndex1 = indexes[0]; |
||||
previousIndex2 = indexes[1]; |
||||
previousIndex3 = indexes[2]; |
||||
|
||||
Arrays.sort(indexes, this); |
||||
facesToTriangulate.addAll(face.detachTriangle(indexes)); |
||||
triangulatedFaces.add(new IndexesLoop(indexes)); |
||||
} |
||||
} |
||||
} |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.WARNING, "Errors occurred during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage()); |
||||
warning = TriangulationWarning.UNKNOWN; |
||||
} |
||||
if(warning != TriangulationWarning.NONE) { |
||||
LOGGER.finest("Triangulation the face using the most direct algorithm."); |
||||
indexes[0] = this.getIndex(0); |
||||
for (int i = 1; i < this.vertexCount() - 1; ++i) { |
||||
indexes[1] = this.getIndex(i); |
||||
indexes[2] = this.getIndex(i + 1); |
||||
triangulatedFaces.add(new IndexesLoop(indexes)); |
||||
} |
||||
} |
||||
return warning; |
||||
} |
||||
|
||||
/** |
||||
* A warning that indicates a problem with face triangulation. The warnings are collected and displayed once for each type for a mesh to |
||||
* avoid multiple warning loggings during triangulation. The amount of iterations can be really huge and logging every single failure would |
||||
* really slow down the importing process and make logs unreadable. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static enum TriangulationWarning { |
||||
NONE(null), |
||||
CLOSEST_VERTS("Unable to find two closest vertices while triangulating face."), |
||||
INFINITE_LOOP("Infinite loop detected during triangulation."), |
||||
UNKNOWN("There was an unknown problem with face triangulation. Please see log for details."); |
||||
|
||||
private String description; |
||||
|
||||
private TriangulationWarning(String description) { |
||||
this.description = description; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return description; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the face is smooth and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isSmooth() { |
||||
return smooth; |
||||
} |
||||
|
||||
/** |
||||
* @return the material index of the face |
||||
*/ |
||||
public int getMaterialNumber() { |
||||
return materialNumber; |
||||
} |
||||
|
||||
/** |
||||
* @return the vertices colord of the face |
||||
*/ |
||||
public List<byte[]> getVertexColors() { |
||||
return vertexColors; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "Face " + indexes; |
||||
} |
||||
|
||||
/** |
||||
* The method finds the closest vertex to the one specified by <b>index</b>. |
||||
* If the vertexToIgnore is positive than it will be ignored in the result. |
||||
* The closest vertex must be able to create an edge that is fully contained |
||||
* within the face and does not cross any other edges. Also if the |
||||
* vertexToIgnore is not negative then the condition that the edge between |
||||
* the found index and the one to ignore is inside the face must also be |
||||
* met. |
||||
* |
||||
* @param index |
||||
* the index of the vertex that needs to have found the nearest |
||||
* neighbour |
||||
* @param indexToIgnore |
||||
* the index to ignore in the result (pass -1 if none is to be |
||||
* ignored) |
||||
* @return the index of the closest vertex to the given one |
||||
*/ |
||||
private int findClosestVertex(int index, int indexToIgnore) { |
||||
int result = -1; |
||||
List<Vector3f> vertices = temporalMesh.getVertices(); |
||||
Vector3f v1 = vertices.get(index); |
||||
float distance = Float.MAX_VALUE; |
||||
for (int i : indexes) { |
||||
if (i != index && i != indexToIgnore) { |
||||
Vector3f v2 = vertices.get(i); |
||||
float d = v2.distance(v1); |
||||
if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh)) && (indexToIgnore < 0 || this.contains(new Edge(indexToIgnore, i, 0, true, temporalMesh)))) { |
||||
result = i; |
||||
distance = d; |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method verifies if the edge is contained within the face. |
||||
* It means it cannot cross any other edge and it must be inside the face and not outside of it. |
||||
* @param edge |
||||
* the edge to be checked |
||||
* @return <b>true</b> if the given edge is contained within the face and <b>false</b> otherwise |
||||
*/ |
||||
private boolean contains(Edge edge) { |
||||
int index1 = edge.getFirstIndex(); |
||||
int index2 = edge.getSecondIndex(); |
||||
// check if the line between the vertices is not a border edge of the face
|
||||
if (!indexes.areNeighbours(index1, index2)) { |
||||
for (int i = 0; i < indexes.size(); ++i) { |
||||
int i1 = this.getIndex(i - 1); |
||||
int i2 = this.getIndex(i); |
||||
// check if the edges have no common verts (because if they do, they cannot cross)
|
||||
if (i1 != index1 && i1 != index2 && i2 != index1 && i2 != index2) { |
||||
if (edge.cross(new Edge(i1, i2, 0, false, temporalMesh))) { |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// computing the edge's middle point
|
||||
Vector3f edgeMiddlePoint = edge.computeCentroid(); |
||||
// computing the edge that is perpendicular to the given edge and has a length of 1 (length actually does not matter)
|
||||
Vector3f edgeVector = edge.getSecondVertex().subtract(edge.getFirstVertex()); |
||||
Vector3f edgeNormal = temporalMesh.getNormals().get(index1).cross(edgeVector).normalizeLocal(); |
||||
Edge e = new Edge(edgeMiddlePoint, edgeNormal.add(edgeMiddlePoint)); |
||||
// compute the vectors from the middle point to the crossing between the extended edge 'e' and other edges of the face
|
||||
List<Vector3f> crossingVectors = new ArrayList<Vector3f>(); |
||||
for (int i = 0; i < indexes.size(); ++i) { |
||||
int i1 = this.getIndex(i); |
||||
int i2 = this.getIndex(i + 1); |
||||
Vector3f crossPoint = e.getCrossPoint(new Edge(i1, i2, 0, false, temporalMesh), true, false); |
||||
if(crossPoint != null) { |
||||
crossingVectors.add(crossPoint.subtractLocal(edgeMiddlePoint)); |
||||
} |
||||
} |
||||
if(crossingVectors.size() == 0) { |
||||
return false;// edges do not cross
|
||||
} |
||||
|
||||
// use only distinct vertices (doubles may appear if the crossing point is a vertex)
|
||||
List<Vector3f> distinctCrossingVectors = new ArrayList<Vector3f>(); |
||||
for(Vector3f cv : crossingVectors) { |
||||
double minDistance = Double.MAX_VALUE; |
||||
for(Vector3f dcv : distinctCrossingVectors) { |
||||
minDistance = Math.min(minDistance, dcv.distance(cv)); |
||||
} |
||||
if(minDistance > FastMath.FLT_EPSILON) { |
||||
distinctCrossingVectors.add(cv); |
||||
} |
||||
} |
||||
|
||||
if(distinctCrossingVectors.size() == 0) { |
||||
throw new IllegalStateException("There MUST be at least 2 crossing vertices!"); |
||||
} |
||||
// checking if all crossing vectors point to the same direction (if yes then the edge is outside the face)
|
||||
float direction = Math.signum(distinctCrossingVectors.get(0).dot(edgeNormal));// if at least one vector has different direction that this - it means that the edge is inside the face
|
||||
for(int i=1;i<distinctCrossingVectors.size();++i) { |
||||
if(direction != Math.signum(distinctCrossingVectors.get(i).dot(edgeNormal))) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
final int prime = 31; |
||||
int result = 1; |
||||
result = prime * result + indexes.hashCode(); |
||||
result = prime * result + temporalMesh.hashCode(); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (!(obj instanceof Face)) { |
||||
return false; |
||||
} |
||||
Face other = (Face) obj; |
||||
if (!indexes.equals(other.indexes)) { |
||||
return false; |
||||
} |
||||
return temporalMesh.equals(other.temporalMesh); |
||||
} |
||||
|
||||
/** |
||||
* Loads all faces of a given mesh. |
||||
* @param meshStructure |
||||
* the mesh structure we read the faces from |
||||
* @param userUVGroups |
||||
* UV groups defined by the user |
||||
* @param verticesColors |
||||
* the vertices colors of the mesh |
||||
* @param temporalMesh |
||||
* the temporal mesh the faces will belong to |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return list of faces read from the given mesh structure |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with file reading occur |
||||
*/ |
||||
public static List<Face> loadAll(Structure meshStructure, Map<String, List<Vector2f>> userUVGroups, List<byte[]> verticesColors, TemporalMesh temporalMesh, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading all faces from mesh: {0}", meshStructure.getName()); |
||||
List<Face> result = new ArrayList<Face>(); |
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
if (meshHelper.isBMeshCompatible(meshStructure)) { |
||||
LOGGER.fine("Reading BMesh."); |
||||
Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop"); |
||||
Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly"); |
||||
|
||||
if (pMPoly.isNotNull() && pMLoop.isNotNull()) { |
||||
List<Structure> polys = pMPoly.fetchData(); |
||||
List<Structure> loops = pMLoop.fetchData(); |
||||
for (Structure poly : polys) { |
||||
int materialNumber = ((Number) poly.getFieldValue("mat_nr")).intValue(); |
||||
int loopStart = ((Number) poly.getFieldValue("loopstart")).intValue(); |
||||
int totLoop = ((Number) poly.getFieldValue("totloop")).intValue(); |
||||
boolean smooth = (((Number) poly.getFieldValue("flag")).byteValue() & 0x01) != 0x00; |
||||
Integer[] vertexIndexes = new Integer[totLoop]; |
||||
|
||||
for (int i = loopStart; i < loopStart + totLoop; ++i) { |
||||
vertexIndexes[i - loopStart] = ((Number) loops.get(i).getFieldValue("v")).intValue(); |
||||
} |
||||
|
||||
// uvs always must be added wheater we have texture or not
|
||||
Map<String, List<Vector2f>> uvCoords = new HashMap<String, List<Vector2f>>(); |
||||
for (Entry<String, List<Vector2f>> entry : userUVGroups.entrySet()) { |
||||
List<Vector2f> uvs = entry.getValue().subList(loopStart, loopStart + totLoop); |
||||
uvCoords.put(entry.getKey(), new ArrayList<Vector2f>(uvs)); |
||||
} |
||||
|
||||
List<byte[]> vertexColors = null; |
||||
if (verticesColors != null && verticesColors.size() > 0) { |
||||
vertexColors = new ArrayList<byte[]>(totLoop); |
||||
for (int i = loopStart; i < loopStart + totLoop; ++i) { |
||||
vertexColors.add(verticesColors.get(i)); |
||||
} |
||||
} |
||||
|
||||
result.add(new Face(vertexIndexes, smooth, materialNumber, uvCoords, vertexColors, temporalMesh)); |
||||
} |
||||
} |
||||
} else { |
||||
LOGGER.fine("Reading traditional faces."); |
||||
Pointer pMFace = (Pointer) meshStructure.getFieldValue("mface"); |
||||
List<Structure> mFaces = pMFace.isNotNull() ? pMFace.fetchData() : null; |
||||
if (mFaces != null && mFaces.size() > 0) { |
||||
// indicates if the material with the specified number should have a texture attached
|
||||
for (int i = 0; i < mFaces.size(); ++i) { |
||||
Structure mFace = mFaces.get(i); |
||||
int materialNumber = ((Number) mFace.getFieldValue("mat_nr")).intValue(); |
||||
boolean smooth = (((Number) mFace.getFieldValue("flag")).byteValue() & 0x01) != 0x00; |
||||
|
||||
int v1 = ((Number) mFace.getFieldValue("v1")).intValue(); |
||||
int v2 = ((Number) mFace.getFieldValue("v2")).intValue(); |
||||
int v3 = ((Number) mFace.getFieldValue("v3")).intValue(); |
||||
int v4 = ((Number) mFace.getFieldValue("v4")).intValue(); |
||||
|
||||
int vertCount = v4 == 0 ? 3 : 4; |
||||
|
||||
// uvs always must be added wheater we have texture or not
|
||||
Map<String, List<Vector2f>> faceUVCoords = new HashMap<String, List<Vector2f>>(); |
||||
for (Entry<String, List<Vector2f>> entry : userUVGroups.entrySet()) { |
||||
List<Vector2f> uvCoordsForASingleFace = new ArrayList<Vector2f>(vertCount); |
||||
for (int j = 0; j < vertCount; ++j) { |
||||
uvCoordsForASingleFace.add(entry.getValue().get(i * 4 + j)); |
||||
} |
||||
faceUVCoords.put(entry.getKey(), uvCoordsForASingleFace); |
||||
} |
||||
|
||||
List<byte[]> vertexColors = null; |
||||
if (verticesColors != null && verticesColors.size() > 0) { |
||||
vertexColors = new ArrayList<byte[]>(vertCount); |
||||
|
||||
vertexColors.add(verticesColors.get(v1)); |
||||
vertexColors.add(verticesColors.get(v2)); |
||||
vertexColors.add(verticesColors.get(v3)); |
||||
if (vertCount == 4) { |
||||
vertexColors.add(verticesColors.get(v4)); |
||||
} |
||||
} |
||||
|
||||
result.add(new Face(vertCount == 4 ? new Integer[] { v1, v2, v3, v4 } : new Integer[] { v1, v2, v3 }, smooth, materialNumber, faceUVCoords, vertexColors, temporalMesh)); |
||||
} |
||||
} |
||||
} |
||||
LOGGER.log(Level.FINE, "Loaded {0} faces.", result.size()); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public int compare(Integer index1, Integer index2) { |
||||
return indexes.indexOf(index1) - indexes.indexOf(index2); |
||||
} |
||||
} |
@ -0,0 +1,305 @@ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.HashMap; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
|
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
|
||||
/** |
||||
* This class represents the Face's indexes loop. It is a simplified implementation of directed graph. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class IndexesLoop implements Comparator<Integer>, Iterable<Integer> { |
||||
public static final IndexPredicate INDEX_PREDICATE_USE_ALL = new IndexPredicate() { |
||||
@Override |
||||
public boolean execute(Integer index) { |
||||
return true; |
||||
} |
||||
}; |
||||
|
||||
/** The indexes. */ |
||||
private List<Integer> nodes; |
||||
/** The edges of the indexes graph. The key is the 'from' index and 'value' is - 'to' index. */ |
||||
private Map<Integer, List<Integer>> edges = new HashMap<Integer, List<Integer>>(); |
||||
|
||||
/** |
||||
* The constructor uses the given nodes in their give order. Each neighbour indexes will form an edge. |
||||
* @param nodes |
||||
* the nodes for the loop |
||||
*/ |
||||
public IndexesLoop(Integer[] nodes) { |
||||
this.nodes = new ArrayList<Integer>(Arrays.asList(nodes)); |
||||
this.prepareEdges(this.nodes); |
||||
} |
||||
|
||||
@Override |
||||
public IndexesLoop clone() { |
||||
return new IndexesLoop(nodes.toArray(new Integer[nodes.size()])); |
||||
} |
||||
|
||||
/** |
||||
* The method prepares edges for the given indexes. |
||||
* @param nodes |
||||
* the indexes |
||||
*/ |
||||
private void prepareEdges(List<Integer> nodes) { |
||||
for (int i = 0; i < nodes.size() - 1; ++i) { |
||||
if (edges.containsKey(nodes.get(i))) { |
||||
edges.get(nodes.get(i)).add(nodes.get(i + 1)); |
||||
} else { |
||||
edges.put(nodes.get(i), new ArrayList<Integer>(Arrays.asList(nodes.get(i + 1)))); |
||||
} |
||||
} |
||||
edges.put(nodes.get(nodes.size() - 1), new ArrayList<Integer>(Arrays.asList(nodes.get(0)))); |
||||
} |
||||
|
||||
/** |
||||
* @return the count of indexes |
||||
*/ |
||||
public int size() { |
||||
return nodes.size(); |
||||
} |
||||
|
||||
/** |
||||
* Adds edge to the loop. |
||||
* @param from |
||||
* the start index |
||||
* @param to |
||||
* the end index |
||||
*/ |
||||
public void addEdge(Integer from, Integer to) { |
||||
if (nodes.contains(from) && nodes.contains(to)) { |
||||
if (edges.containsKey(from) && !edges.get(from).contains(to)) { |
||||
edges.get(from).add(to); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Removes edge from the face. The edge is removed if it already exists in the face. |
||||
* @param node1 |
||||
* the first index of the edge to be removed |
||||
* @param node2 |
||||
* the second index of the edge to be removed |
||||
* @return <b>true</b> if the edge was removed and <b>false</b> otherwise |
||||
*/ |
||||
public boolean removeEdge(Integer node1, Integer node2) { |
||||
boolean edgeRemoved = false; |
||||
if (nodes.contains(node1) && nodes.contains(node2)) { |
||||
if (edges.containsKey(node1)) { |
||||
edgeRemoved |= edges.get(node1).remove(node2); |
||||
} |
||||
if (edges.containsKey(node2)) { |
||||
edgeRemoved |= edges.get(node2).remove(node1); |
||||
} |
||||
if (edgeRemoved) { |
||||
if (this.getNeighbourCount(node1) == 0) { |
||||
this.removeIndexes(node1); |
||||
} |
||||
if (this.getNeighbourCount(node2) == 0) { |
||||
this.removeIndexes(node2); |
||||
} |
||||
} |
||||
} |
||||
return edgeRemoved; |
||||
} |
||||
|
||||
/** |
||||
* Tells if the given indexes are neighbours. |
||||
* @param index1 |
||||
* the first index |
||||
* @param index2 |
||||
* the second index |
||||
* @return <b>true</b> if the given indexes are neighbours and <b>false</b> otherwise |
||||
*/ |
||||
public boolean areNeighbours(Integer index1, Integer index2) { |
||||
if (index1.equals(index2) || !edges.containsKey(index1) || !edges.containsKey(index2)) { |
||||
return false; |
||||
} |
||||
return edges.get(index1).contains(index2) || edges.get(index2).contains(index1); |
||||
} |
||||
|
||||
/** |
||||
* Returns the value of the index located after the given one. Pointint the last index will return the first one. |
||||
* @param index |
||||
* the index value |
||||
* @return the value of 'next' index |
||||
*/ |
||||
public Integer getNextIndex(Integer index) { |
||||
int i = nodes.indexOf(index); |
||||
return i == nodes.size() - 1 ? nodes.get(0) : nodes.get(i + 1); |
||||
} |
||||
|
||||
/** |
||||
* Returns the value of the index located before the given one. Pointint the first index will return the last one. |
||||
* @param index |
||||
* the index value |
||||
* @return the value of 'previous' index |
||||
*/ |
||||
public Integer getPreviousIndex(Integer index) { |
||||
int i = nodes.indexOf(index); |
||||
return i == 0 ? nodes.get(nodes.size() - 1) : nodes.get(i - 1); |
||||
} |
||||
|
||||
/** |
||||
* The method shifts all indexes by a given value. |
||||
* @param shift |
||||
* the value to shift all indexes |
||||
* @param predicate |
||||
* the predicate that verifies which indexes should be shifted; if null then all will be shifted |
||||
*/ |
||||
public void shiftIndexes(int shift, IndexPredicate predicate) { |
||||
if (predicate == null) { |
||||
predicate = INDEX_PREDICATE_USE_ALL; |
||||
} |
||||
List<Integer> nodes = new ArrayList<Integer>(this.nodes.size()); |
||||
for (Integer node : this.nodes) { |
||||
nodes.add(node + (predicate.execute(node) ? shift : 0)); |
||||
} |
||||
|
||||
Map<Integer, List<Integer>> edges = new HashMap<Integer, List<Integer>>(); |
||||
for (Entry<Integer, List<Integer>> entry : this.edges.entrySet()) { |
||||
List<Integer> neighbours = new ArrayList<Integer>(entry.getValue().size()); |
||||
for (Integer neighbour : entry.getValue()) { |
||||
neighbours.add(neighbour + (predicate.execute(neighbour) ? shift : 0)); |
||||
} |
||||
edges.put(entry.getKey() + shift, neighbours); |
||||
} |
||||
|
||||
this.nodes = nodes; |
||||
this.edges = edges; |
||||
} |
||||
|
||||
/** |
||||
* Reverses the order of the indexes. |
||||
*/ |
||||
public void reverse() { |
||||
Collections.reverse(nodes); |
||||
edges.clear(); |
||||
this.prepareEdges(nodes); |
||||
} |
||||
|
||||
/** |
||||
* Returns the neighbour count of the given index. |
||||
* @param index |
||||
* the index whose neighbour count will be checked |
||||
* @return the count of neighbours of the given index |
||||
*/ |
||||
private int getNeighbourCount(Integer index) { |
||||
int result = 0; |
||||
if (edges.containsKey(index)) { |
||||
result = edges.get(index).size(); |
||||
for (List<Integer> neighbours : edges.values()) { |
||||
if (neighbours.contains(index)) { |
||||
++result; |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Returns the position of the given index in the loop. |
||||
* @param index |
||||
* the index of the face |
||||
* @return the indexe's position in the loop |
||||
*/ |
||||
public int indexOf(Integer index) { |
||||
return nodes.indexOf(index); |
||||
} |
||||
|
||||
/** |
||||
* Returns the index at the given position. |
||||
* @param indexPosition |
||||
* the position of the index |
||||
* @return the index at a given position |
||||
*/ |
||||
public Integer get(int indexPosition) { |
||||
return nodes.get(indexPosition); |
||||
} |
||||
|
||||
/** |
||||
* @return all indexes of the face |
||||
*/ |
||||
public List<Integer> getAll() { |
||||
return new ArrayList<Integer>(nodes); |
||||
} |
||||
|
||||
/** |
||||
* The method removes all given indexes. |
||||
* @param indexes |
||||
* the indexes to be removed |
||||
*/ |
||||
public void removeIndexes(Integer... indexes) { |
||||
for (Integer index : indexes) { |
||||
nodes.remove(index); |
||||
edges.remove(index); |
||||
for (List<Integer> neighbours : edges.values()) { |
||||
neighbours.remove(index); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method finds the path between the given indexes. |
||||
* @param start |
||||
* the start index |
||||
* @param end |
||||
* the end index |
||||
* @param result |
||||
* a list containing indexes on the path from start to end (inclusive) |
||||
* @throws IllegalStateException |
||||
* an exception is thrown when the loop is not normalized (at least one |
||||
* index has more than 2 neighbours) |
||||
* @throws BlenderFileException |
||||
* an exception is thrown if the vertices of a face create more than one loop; this is thrown |
||||
* to prevent lack of memory errors during triangulation |
||||
*/ |
||||
public void findPath(Integer start, Integer end, List<Integer> result) throws BlenderFileException { |
||||
result.clear(); |
||||
Integer node = start; |
||||
while (!node.equals(end)) { |
||||
if (result.contains(node)) { |
||||
throw new BlenderFileException("Indexes of face have infinite loops!"); |
||||
} |
||||
result.add(node); |
||||
List<Integer> nextSteps = edges.get(node); |
||||
if (nextSteps == null || nextSteps.size() == 0) { |
||||
result.clear();// no directed path from start to end
|
||||
return; |
||||
} else if (nextSteps.size() == 1) { |
||||
node = nextSteps.get(0); |
||||
} else { |
||||
throw new BlenderFileException("Triangulation failed. Face has ambiguous indexes loop. Please triangulate your model in Blender as a workaround."); |
||||
} |
||||
} |
||||
result.add(end); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "IndexesLoop " + nodes.toString(); |
||||
} |
||||
|
||||
@Override |
||||
public int compare(Integer i1, Integer i2) { |
||||
return nodes.indexOf(i1) - nodes.indexOf(i2); |
||||
} |
||||
|
||||
@Override |
||||
public Iterator<Integer> iterator() { |
||||
return nodes.iterator(); |
||||
} |
||||
|
||||
public static interface IndexPredicate { |
||||
boolean execute(Integer index); |
||||
} |
||||
} |
@ -0,0 +1,364 @@ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.nio.Buffer; |
||||
import java.nio.ByteBuffer; |
||||
import java.nio.FloatBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.NavigableMap; |
||||
import java.util.TreeMap; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.VertexBuffer; |
||||
import com.jme3.scene.VertexBuffer.Format; |
||||
import com.jme3.scene.VertexBuffer.Type; |
||||
import com.jme3.scene.VertexBuffer.Usage; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
/** |
||||
* A class that aggregates the mesh data to prepare proper buffers. The buffers refer only to ONE material. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class MeshBuffers { |
||||
private static final int MAXIMUM_WEIGHTS_PER_VERTEX = 4; |
||||
|
||||
/** The material index. */ |
||||
private final int materialIndex; |
||||
/** The vertices. */ |
||||
private List<Vector3f> verts = new ArrayList<Vector3f>(); |
||||
/** The normals. */ |
||||
private List<Vector3f> normals = new ArrayList<Vector3f>(); |
||||
/** The UV coordinate sets. */ |
||||
private Map<String, List<Vector2f>> uvCoords = new HashMap<String, List<Vector2f>>(); |
||||
/** The vertex colors. */ |
||||
private List<byte[]> vertColors = new ArrayList<byte[]>(); |
||||
/** The indexes. */ |
||||
private List<Integer> indexes = new ArrayList<Integer>(); |
||||
/** The maximum weights count assigned to a single vertex. Used during weights normalization. */ |
||||
private int maximumWeightsPerVertex; |
||||
/** A list of mapping between weights and indexes. Each entry for the proper vertex. */ |
||||
private List<TreeMap<Float, Integer>> boneWeightAndIndexes = new ArrayList<TreeMap<Float, Integer>>(); |
||||
|
||||
/** |
||||
* Constructor stores only the material index value. |
||||
* @param materialIndex |
||||
* the material index |
||||
*/ |
||||
public MeshBuffers(int materialIndex) { |
||||
this.materialIndex = materialIndex; |
||||
} |
||||
|
||||
/** |
||||
* @return the material index |
||||
*/ |
||||
public int getMaterialIndex() { |
||||
return materialIndex; |
||||
} |
||||
|
||||
/** |
||||
* @return indexes buffer |
||||
*/ |
||||
public Buffer getIndexBuffer() { |
||||
if (indexes.size() <= Short.MAX_VALUE) { |
||||
short[] indices = new short[indexes.size()]; |
||||
for (int i = 0; i < indexes.size(); ++i) { |
||||
indices[i] = indexes.get(i).shortValue(); |
||||
} |
||||
return BufferUtils.createShortBuffer(indices); |
||||
} else { |
||||
int[] indices = new int[indexes.size()]; |
||||
for (int i = 0; i < indexes.size(); ++i) { |
||||
indices[i] = indexes.get(i).intValue(); |
||||
} |
||||
return BufferUtils.createIntBuffer(indices); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return positions buffer |
||||
*/ |
||||
public VertexBuffer getPositionsBuffer() { |
||||
VertexBuffer positionBuffer = new VertexBuffer(Type.Position); |
||||
Vector3f[] data = verts.toArray(new Vector3f[verts.size()]); |
||||
positionBuffer.setupData(Usage.Static, 3, Format.Float, BufferUtils.createFloatBuffer(data)); |
||||
return positionBuffer; |
||||
} |
||||
|
||||
/** |
||||
* @return normals buffer |
||||
*/ |
||||
public VertexBuffer getNormalsBuffer() { |
||||
VertexBuffer positionBuffer = new VertexBuffer(Type.Normal); |
||||
Vector3f[] data = normals.toArray(new Vector3f[normals.size()]); |
||||
positionBuffer.setupData(Usage.Static, 3, Format.Float, BufferUtils.createFloatBuffer(data)); |
||||
return positionBuffer; |
||||
} |
||||
|
||||
/** |
||||
* @return bone buffers |
||||
*/ |
||||
public BoneBuffersData getBoneBuffers() { |
||||
BoneBuffersData result = null; |
||||
if (maximumWeightsPerVertex > 0) { |
||||
this.normalizeBoneBuffers(MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
maximumWeightsPerVertex = MAXIMUM_WEIGHTS_PER_VERTEX; |
||||
|
||||
FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(boneWeightAndIndexes.size() * MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
ByteBuffer indicesData = BufferUtils.createByteBuffer(boneWeightAndIndexes.size() * MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
int index = 0; |
||||
for (Map<Float, Integer> boneBuffersData : boneWeightAndIndexes) { |
||||
if (boneBuffersData.size() > 0) { |
||||
int count = 0; |
||||
for (Entry<Float, Integer> entry : boneBuffersData.entrySet()) { |
||||
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getKey()); |
||||
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getValue().byteValue()); |
||||
++count; |
||||
} |
||||
} else { |
||||
// if no bone is assigned to this vertex then attach it to the 0-indexed root bone
|
||||
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f); |
||||
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0); |
||||
} |
||||
++index; |
||||
} |
||||
VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight); |
||||
verticesWeights.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.Float, weightsFloatData); |
||||
|
||||
VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex); |
||||
verticesWeightsIndices.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.UnsignedByte, indicesData); |
||||
|
||||
result = new BoneBuffersData(maximumWeightsPerVertex, verticesWeights, verticesWeightsIndices); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return UV coordinates sets |
||||
*/ |
||||
public Map<String, List<Vector2f>> getUvCoords() { |
||||
return uvCoords; |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if vertex colors are used and <b>false</b> otherwise |
||||
*/ |
||||
public boolean areVertexColorsUsed() { |
||||
return vertColors.size() > 0; |
||||
} |
||||
|
||||
/** |
||||
* @return vertex colors buffer |
||||
*/ |
||||
public ByteBuffer getVertexColorsBuffer() { |
||||
ByteBuffer result = null; |
||||
if (vertColors.size() > 0) { |
||||
result = BufferUtils.createByteBuffer(4 * vertColors.size()); |
||||
for (byte[] v : vertColors) { |
||||
if (v != null) { |
||||
result.put(v[0]).put(v[1]).put(v[2]).put(v[3]); |
||||
} else { |
||||
result.put((byte) 0).put((byte) 0).put((byte) 0).put((byte) 0); |
||||
} |
||||
} |
||||
result.flip(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if indexes can be shorts' and <b>false</b> if they need to be ints' |
||||
*/ |
||||
public boolean isShortIndexBuffer() { |
||||
return indexes.size() <= Short.MAX_VALUE; |
||||
} |
||||
|
||||
/** |
||||
* Appends a vertex and normal to the buffers. |
||||
* @param vert |
||||
* vertex |
||||
* @param normal |
||||
* normal vector |
||||
*/ |
||||
public void append(Vector3f vert, Vector3f normal) { |
||||
int index = this.indexOf(vert, normal, null); |
||||
if (index >= 0) { |
||||
indexes.add(index); |
||||
} else { |
||||
indexes.add(verts.size()); |
||||
verts.add(vert); |
||||
normals.add(normal); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Appends the face data to the buffers. |
||||
* @param smooth |
||||
* tells if the face is smooth or flat |
||||
* @param verts |
||||
* the vertices |
||||
* @param normals |
||||
* the normals |
||||
* @param uvCoords |
||||
* the UV coordinates |
||||
* @param vertColors |
||||
* the vertex colors |
||||
* @param vertexGroups |
||||
* the vertex groups |
||||
*/ |
||||
public void append(boolean smooth, Vector3f[] verts, Vector3f[] normals, Map<String, List<Vector2f>> uvCoords, byte[][] vertColors, List<Map<Float, Integer>> vertexGroups) { |
||||
if (verts.length != normals.length) { |
||||
throw new IllegalArgumentException("The amount of verts and normals MUST be equal!"); |
||||
} |
||||
if (vertColors != null && vertColors.length != verts.length) { |
||||
throw new IllegalArgumentException("The amount of vertex colors and vertices MUST be equal!"); |
||||
} |
||||
if (vertexGroups.size() != 0 && vertexGroups.size() != verts.length) { |
||||
throw new IllegalArgumentException("The amount of (if given) vertex groups and vertices MUST be equal!"); |
||||
} |
||||
|
||||
if (!smooth) { |
||||
// make the normals perpendicular to the face
|
||||
normals[0] = normals[1] = normals[2] = FastMath.computeNormal(verts[0], verts[1], verts[2]); |
||||
} |
||||
|
||||
for (int i = 0; i < verts.length; ++i) { |
||||
int index = -1; |
||||
Map<String, Vector2f> uvCoordsForVertex = this.getUVsForVertex(i, uvCoords); |
||||
if (smooth && (index = this.indexOf(verts[i], normals[i], uvCoordsForVertex)) >= 0) { |
||||
indexes.add(index); |
||||
} else { |
||||
indexes.add(this.verts.size()); |
||||
this.verts.add(verts[i]); |
||||
this.normals.add(normals[i]); |
||||
this.vertColors.add(vertColors[i]); |
||||
|
||||
if (uvCoords != null && uvCoords.size() > 0) { |
||||
for (Entry<String, List<Vector2f>> entry : uvCoords.entrySet()) { |
||||
if (this.uvCoords.containsKey(entry.getKey())) { |
||||
this.uvCoords.get(entry.getKey()).add(entry.getValue().get(i)); |
||||
} else { |
||||
List<Vector2f> uvs = new ArrayList<Vector2f>(); |
||||
uvs.add(entry.getValue().get(i)); |
||||
this.uvCoords.put(entry.getKey(), uvs); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (vertexGroups != null && vertexGroups.size() > 0) { |
||||
Map<Float, Integer> group = vertexGroups.get(i); |
||||
maximumWeightsPerVertex = Math.max(maximumWeightsPerVertex, group.size()); |
||||
boneWeightAndIndexes.add(new TreeMap<Float, Integer>(group)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns UV coordinates assigned for the vertex with the proper index. |
||||
* @param vertexIndex |
||||
* the index of the vertex |
||||
* @param uvs |
||||
* all UV coordinates we search in |
||||
* @return a set of UV coordinates assigned to the given vertex |
||||
*/ |
||||
private Map<String, Vector2f> getUVsForVertex(int vertexIndex, Map<String, List<Vector2f>> uvs) { |
||||
if (uvs == null || uvs.size() == 0) { |
||||
return null; |
||||
} |
||||
Map<String, Vector2f> result = new HashMap<String, Vector2f>(uvs.size()); |
||||
for (Entry<String, List<Vector2f>> entry : uvs.entrySet()) { |
||||
result.put(entry.getKey(), entry.getValue().get(vertexIndex)); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method returns an index of a vertex described by the given data. |
||||
* The method tries to find a vertex that mathes the given data. If it does it means |
||||
* that such vertex is already used. |
||||
* @param vert |
||||
* the vertex position coordinates |
||||
* @param normal |
||||
* the vertex's normal vector |
||||
* @param uvCoords |
||||
* the UV coords of the vertex |
||||
* @return index of the found vertex of -1 |
||||
*/ |
||||
private int indexOf(Vector3f vert, Vector3f normal, Map<String, Vector2f> uvCoords) { |
||||
for (int i = 0; i < verts.size(); ++i) { |
||||
if (verts.get(i).equals(vert) && normals.get(i).equals(normal)) { |
||||
if (uvCoords != null && uvCoords.size() > 0) { |
||||
for (Entry<String, Vector2f> entry : uvCoords.entrySet()) { |
||||
List<Vector2f> uvs = this.uvCoords.get(entry.getKey()); |
||||
if (uvs == null) { |
||||
return -1; |
||||
} |
||||
if (!uvs.get(i).equals(entry.getValue())) { |
||||
return -1; |
||||
} |
||||
} |
||||
} |
||||
return i; |
||||
} |
||||
} |
||||
return -1; |
||||
} |
||||
|
||||
/** |
||||
* The method normalizes the weights and bone indexes data. |
||||
* First it truncates the amount to MAXIMUM_WEIGHTS_PER_VERTEX because this is how many weights JME can handle. |
||||
* Next it normalizes the weights so that the sum of all verts is 1. |
||||
* @param maximumSize |
||||
* the maximum size that the data will be truncated to (usually: MAXIMUM_WEIGHTS_PER_VERTEX) |
||||
*/ |
||||
private void normalizeBoneBuffers(int maximumSize) { |
||||
for (TreeMap<Float, Integer> group : boneWeightAndIndexes) { |
||||
if (group.size() > maximumSize) { |
||||
NavigableMap<Float, Integer> descendingWeights = group.descendingMap(); |
||||
while (descendingWeights.size() > maximumSize) { |
||||
descendingWeights.pollLastEntry(); |
||||
} |
||||
} |
||||
|
||||
// normalizing the weights so that the sum of the values is equal to '1'
|
||||
TreeMap<Float, Integer> normalizedGroup = new TreeMap<Float, Integer>(); |
||||
float sum = 0; |
||||
for (Entry<Float, Integer> entry : group.entrySet()) { |
||||
sum += entry.getKey(); |
||||
} |
||||
|
||||
if (sum != 0 && sum != 1) { |
||||
for (Entry<Float, Integer> entry : group.entrySet()) { |
||||
normalizedGroup.put(entry.getKey() / sum, entry.getValue()); |
||||
} |
||||
group.clear(); |
||||
group.putAll(normalizedGroup); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A class that gathers the data for mesh bone buffers. |
||||
* Added to increase code readability. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public static class BoneBuffersData { |
||||
public final int maximumWeightsPerVertex; |
||||
public final VertexBuffer verticesWeights; |
||||
public final VertexBuffer verticesWeightsIndices; |
||||
|
||||
public BoneBuffersData(int maximumWeightsPerVertex, VertexBuffer verticesWeights, VertexBuffer verticesWeightsIndices) { |
||||
this.maximumWeightsPerVertex = maximumWeightsPerVertex; |
||||
this.verticesWeights = verticesWeights; |
||||
this.verticesWeightsIndices = verticesWeightsIndices; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,381 @@ |
||||
/* |
||||
* Copyright (c) 2009-2019 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.material.Material; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialHelper; |
||||
import com.jme3.scene.plugins.blender.objects.Properties; |
||||
|
||||
/** |
||||
* A class that is used in mesh calculations. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class MeshHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(MeshHelper.class.getName()); |
||||
|
||||
/** A type of UV data layer in traditional faced mesh (triangles or quads). */ |
||||
public static final int UV_DATA_LAYER_TYPE_FMESH = 5; |
||||
/** A type of UV data layer in bmesh type. */ |
||||
public static final int UV_DATA_LAYER_TYPE_BMESH = 16; |
||||
|
||||
/** A material used for single lines and points. */ |
||||
private Material blackUnshadedMaterial; |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender |
||||
* versions. |
||||
* |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public MeshHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* Converts the mesh structure into temporal mesh. |
||||
* The temporal mesh is stored in blender context and here always a clone is being returned because the mesh might |
||||
* be modified by modifiers. |
||||
* |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return temporal mesh read from the given structure |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading blend file occur |
||||
*/ |
||||
public TemporalMesh toTemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading temporal mesh named: {0}.", meshStructure.getName()); |
||||
TemporalMesh temporalMesh = (TemporalMesh) blenderContext.getLoadedFeature(meshStructure.getOldMemoryAddress(), LoadedDataType.TEMPORAL_MESH); |
||||
if (temporalMesh != null) { |
||||
LOGGER.fine("The mesh is already loaded. Returning its clone."); |
||||
return temporalMesh.clone(); |
||||
} |
||||
|
||||
if ("ID".equals(meshStructure.getType())) { |
||||
LOGGER.fine("Loading mesh from external blend file."); |
||||
return (TemporalMesh) this.loadLibrary(meshStructure); |
||||
} |
||||
|
||||
String name = meshStructure.getName(); |
||||
LOGGER.log(Level.FINE, "Reading mesh: {0}.", name); |
||||
temporalMesh = new TemporalMesh(meshStructure, blenderContext); |
||||
|
||||
LOGGER.fine("Loading materials."); |
||||
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); |
||||
temporalMesh.setMaterials(materialHelper.getMaterials(meshStructure, blenderContext)); |
||||
|
||||
LOGGER.fine("Reading custom properties."); |
||||
Properties properties = this.loadProperties(meshStructure, blenderContext); |
||||
temporalMesh.setProperties(properties); |
||||
|
||||
blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, meshStructure); |
||||
blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.TEMPORAL_MESH, temporalMesh); |
||||
return temporalMesh.clone(); |
||||
} |
||||
|
||||
/** |
||||
* Tells if the given mesh structure supports BMesh. |
||||
* |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @return <b>true</b> if BMesh is supported and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isBMeshCompatible(Structure meshStructure) { |
||||
Pointer pMLoop = (Pointer) meshStructure.getFieldValue("mloop"); |
||||
Pointer pMPoly = (Pointer) meshStructure.getFieldValue("mpoly"); |
||||
return pMLoop != null && pMPoly != null && pMLoop.isNotNull() && pMPoly.isNotNull(); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the vertices: a list of vertex positions and a list |
||||
* of vertex normals. |
||||
* |
||||
* @param meshStructure |
||||
* the structure containing the mesh data |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is somehow invalid or corrupted |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public void loadVerticesAndNormals(Structure meshStructure, List<Vector3f> vertices, List<Vector3f> normals) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading vertices and normals from mesh: {0}.", meshStructure.getName()); |
||||
int count = ((Number) meshStructure.getFieldValue("totvert")).intValue(); |
||||
if (count > 0) { |
||||
Pointer pMVert = (Pointer) meshStructure.getFieldValue("mvert"); |
||||
List<Structure> mVerts = pMVert.fetchData(); |
||||
Vector3f co = null, no = null; |
||||
if (fixUpAxis) { |
||||
for (int i = 0; i < count; ++i) { |
||||
DynamicArray<Number> coordinates = (DynamicArray<Number>) mVerts.get(i).getFieldValue("co"); |
||||
co = new Vector3f(coordinates.get(0).floatValue(), coordinates.get(2).floatValue(), -coordinates.get(1).floatValue()); |
||||
vertices.add(co); |
||||
|
||||
DynamicArray<Number> norm = (DynamicArray<Number>) mVerts.get(i).getFieldValue("no"); |
||||
no = new Vector3f(norm.get(0).shortValue() / 32767.0f, norm.get(2).shortValue() / 32767.0f, -norm.get(1).shortValue() / 32767.0f); |
||||
normals.add(no); |
||||
} |
||||
} else { |
||||
for (int i = 0; i < count; ++i) { |
||||
DynamicArray<Number> coordinates = (DynamicArray<Number>) mVerts.get(i).getFieldValue("co"); |
||||
co = new Vector3f(coordinates.get(0).floatValue(), coordinates.get(1).floatValue(), coordinates.get(2).floatValue()); |
||||
vertices.add(co); |
||||
|
||||
DynamicArray<Number> norm = (DynamicArray<Number>) mVerts.get(i).getFieldValue("no"); |
||||
no = new Vector3f(norm.get(0).shortValue() / 32767.0f, norm.get(1).shortValue() / 32767.0f, norm.get(2).shortValue() / 32767.0f); |
||||
normals.add(no); |
||||
} |
||||
} |
||||
} |
||||
LOGGER.log(Level.FINE, "Loaded {0} vertices and normals.", vertices.size()); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the vertices colors. Each vertex is stored in byte[4] array. |
||||
* |
||||
* @param meshStructure |
||||
* the structure containing the mesh data |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return a list of vertices colors, each color belongs to a single vertex or empty list of colors are not specified |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is somehow invalid or corrupted |
||||
*/ |
||||
public List<byte[]> loadVerticesColors(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading vertices colors from mesh: {0}.", meshStructure.getName()); |
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
Pointer pMCol = (Pointer) meshStructure.getFieldValue(meshHelper.isBMeshCompatible(meshStructure) ? "mloopcol" : "mcol"); |
||||
List<byte[]> verticesColors = new ArrayList<byte[]>(); |
||||
// it was likely a bug in blender untill version 2.63 (the blue and red factors were misplaced in their structure)
|
||||
// so we need to put them right
|
||||
boolean useBGRA = blenderContext.getBlenderVersion() < 263; |
||||
if (pMCol.isNotNull()) { |
||||
List<Structure> mCol = pMCol.fetchData(); |
||||
for (Structure color : mCol) { |
||||
byte r = ((Number) color.getFieldValue("r")).byteValue(); |
||||
byte g = ((Number) color.getFieldValue("g")).byteValue(); |
||||
byte b = ((Number) color.getFieldValue("b")).byteValue(); |
||||
byte a = ((Number) color.getFieldValue("a")).byteValue(); |
||||
verticesColors.add(useBGRA ? new byte[] { b, g, r, a } : new byte[] { r, g, b, a }); |
||||
} |
||||
} |
||||
return verticesColors; |
||||
} |
||||
|
||||
/** |
||||
* The method loads the UV coordinates. The result is a map where the key is the user's UV set name and the values are UV coordinates. |
||||
* But depending on the mesh type (triangle/quads or bmesh) the lists in the map have different meaning. |
||||
* For bmesh they are enlisted just like they are stored in the blend file (in loops). |
||||
* For traditional faces every 4 UV's should be assigned for a single face. |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @return a map that sorts UV coordinates between different UV sets |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with blend file occur |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public LinkedHashMap<String, List<Vector2f>> loadUVCoordinates(Structure meshStructure) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading UV coordinates from mesh: {0}.", meshStructure.getName()); |
||||
LinkedHashMap<String, List<Vector2f>> result = new LinkedHashMap<String, List<Vector2f>>(); |
||||
if (this.isBMeshCompatible(meshStructure)) { |
||||
// in this case the UV's are assigned to vertices (an array is the same length as the vertex array)
|
||||
Structure loopData = (Structure) meshStructure.getFieldValue("ldata"); |
||||
Pointer pLoopDataLayers = (Pointer) loopData.getFieldValue("layers"); |
||||
List<Structure> loopDataLayers = pLoopDataLayers.fetchData(); |
||||
for (Structure structure : loopDataLayers) { |
||||
Pointer p = (Pointer) structure.getFieldValue("data"); |
||||
if (p.isNotNull() && ((Number) structure.getFieldValue("type")).intValue() == MeshHelper.UV_DATA_LAYER_TYPE_BMESH) { |
||||
String uvSetName = structure.getFieldValue("name").toString(); |
||||
List<Structure> uvsStructures = p.fetchData(); |
||||
List<Vector2f> uvs = new ArrayList<Vector2f>(uvsStructures.size()); |
||||
for (Structure uvStructure : uvsStructures) { |
||||
DynamicArray<Number> loopUVS = (DynamicArray<Number>) uvStructure.getFieldValue("uv"); |
||||
uvs.add(new Vector2f(loopUVS.get(0).floatValue(), loopUVS.get(1).floatValue())); |
||||
} |
||||
result.put(uvSetName, uvs); |
||||
} |
||||
} |
||||
} else { |
||||
// in this case UV's are assigned to faces (the array has the same length as the faces count)
|
||||
Structure facesData = (Structure) meshStructure.getFieldValue("fdata"); |
||||
Pointer pFacesDataLayers = (Pointer) facesData.getFieldValue("layers"); |
||||
if (pFacesDataLayers.isNotNull()) { |
||||
List<Structure> facesDataLayers = pFacesDataLayers.fetchData(); |
||||
for (Structure structure : facesDataLayers) { |
||||
Pointer p = (Pointer) structure.getFieldValue("data"); |
||||
if (p.isNotNull() && ((Number) structure.getFieldValue("type")).intValue() == MeshHelper.UV_DATA_LAYER_TYPE_FMESH) { |
||||
String uvSetName = structure.getFieldValue("name").toString(); |
||||
List<Structure> uvsStructures = p.fetchData(); |
||||
List<Vector2f> uvs = new ArrayList<Vector2f>(uvsStructures.size()); |
||||
for (Structure uvStructure : uvsStructures) { |
||||
DynamicArray<Number> mFaceUVs = (DynamicArray<Number>) uvStructure.getFieldValue("uv"); |
||||
uvs.add(new Vector2f(mFaceUVs.get(0).floatValue(), mFaceUVs.get(1).floatValue())); |
||||
uvs.add(new Vector2f(mFaceUVs.get(2).floatValue(), mFaceUVs.get(3).floatValue())); |
||||
uvs.add(new Vector2f(mFaceUVs.get(4).floatValue(), mFaceUVs.get(5).floatValue())); |
||||
uvs.add(new Vector2f(mFaceUVs.get(6).floatValue(), mFaceUVs.get(7).floatValue())); |
||||
} |
||||
result.put(uvSetName, uvs); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Loads all vertices groups. |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @return a list of vertex groups for every vertex in the mesh |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with blend file occur |
||||
*/ |
||||
public List<Map<String, Float>> loadVerticesGroups(Structure meshStructure) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading vertices groups from mesh: {0}.", meshStructure.getName()); |
||||
List<Map<String, Float>> result = new ArrayList<Map<String, Float>>(); |
||||
|
||||
Structure parent = blenderContext.peekParent(); |
||||
if(parent != null) { |
||||
// the mesh might be saved without its parent (it is then unused)
|
||||
Structure defbase = (Structure) parent.getFieldValue("defbase"); |
||||
List<String> groupNames = new ArrayList<String>(); |
||||
List<Structure> defs = defbase.evaluateListBase(); |
||||
|
||||
if(!defs.isEmpty()) { |
||||
for (Structure def : defs) { |
||||
groupNames.add(def.getFieldValue("name").toString()); |
||||
} |
||||
|
||||
Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
|
||||
if (pDvert.isNotNull()) {// assigning weights and bone indices
|
||||
List<Structure> dverts = pDvert.fetchData(); |
||||
for (Structure dvert : dverts) { |
||||
Map<String, Float> weightsForVertex = new HashMap<String, Float>(); |
||||
Pointer pDW = (Pointer) dvert.getFieldValue("dw"); |
||||
if (pDW.isNotNull()) { |
||||
List<Structure> dw = pDW.fetchData(); |
||||
for (Structure deformWeight : dw) { |
||||
int groupIndex = ((Number) deformWeight.getFieldValue("def_nr")).intValue(); |
||||
float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue(); |
||||
String groupName = groupNames.get(groupIndex); |
||||
|
||||
weightsForVertex.put(groupName, weight); |
||||
} |
||||
} |
||||
result.add(weightsForVertex); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Selects the proper subsets of UV coordinates for the given sublist of indexes. |
||||
* @param face |
||||
* the face with the original UV sets |
||||
* @param indexesSublist |
||||
* the sub list of indexes |
||||
* @return a map of UV coordinates subsets |
||||
*/ |
||||
public Map<String, List<Vector2f>> selectUVSubset(Face face, Integer... indexesSublist) { |
||||
Map<String, List<Vector2f>> result = null; |
||||
if (face.getUvSets() != null) { |
||||
result = new HashMap<String, List<Vector2f>>(); |
||||
for (Entry<String, List<Vector2f>> entry : face.getUvSets().entrySet()) { |
||||
List<Vector2f> uvs = new ArrayList<Vector2f>(indexesSublist.length); |
||||
for (Integer index : indexesSublist) { |
||||
uvs.add(entry.getValue().get(face.getIndexes().indexOf(index))); |
||||
} |
||||
result.put(entry.getKey(), uvs); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Selects the proper subsets of vertex colors for the given sublist of indexes. |
||||
* @param face |
||||
* the face with the original vertex colors |
||||
* @param indexesSublist |
||||
* the sub list of indexes |
||||
* @return a sublist of vertex colors |
||||
*/ |
||||
public List<byte[]> selectVertexColorSubset(Face face, Integer... indexesSublist) { |
||||
List<byte[]> result = null; |
||||
List<byte[]> vertexColors = face.getVertexColors(); |
||||
if (vertexColors != null) { |
||||
result = new ArrayList<byte[]>(indexesSublist.length); |
||||
for (Integer index : indexesSublist) { |
||||
result.add(vertexColors.get(face.getIndexes().indexOf(index))); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Returns the black unshaded material. It is used for lines and points because that is how blender |
||||
* renders it. |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return black unshaded material |
||||
*/ |
||||
public synchronized Material getBlackUnshadedMaterial(BlenderContext blenderContext) { |
||||
if (blackUnshadedMaterial == null) { |
||||
blackUnshadedMaterial = new Material(blenderContext.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); |
||||
blackUnshadedMaterial.setColor("Color", ColorRGBA.Black); |
||||
} |
||||
return blackUnshadedMaterial; |
||||
} |
||||
} |
@ -0,0 +1,93 @@ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate; |
||||
|
||||
/** |
||||
* A class that represents a single point on the scene that is not a part of an edge. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class Point { |
||||
private static final Logger LOGGER = Logger.getLogger(Point.class.getName()); |
||||
|
||||
/** The point's index. */ |
||||
private int index; |
||||
|
||||
/** |
||||
* Constructs a point for a given index. |
||||
* @param index |
||||
* the index of the point |
||||
*/ |
||||
public Point(int index) { |
||||
this.index = index; |
||||
} |
||||
|
||||
@Override |
||||
public Point clone() { |
||||
return new Point(index); |
||||
} |
||||
|
||||
/** |
||||
* @return the index of the point |
||||
*/ |
||||
public int getIndex() { |
||||
return index; |
||||
} |
||||
|
||||
/** |
||||
* The method shifts the index by a given value. |
||||
* @param shift |
||||
* the value to shift the index |
||||
* @param predicate |
||||
* the predicate that verifies which indexes should be shifted; if null then all will be shifted |
||||
*/ |
||||
public void shiftIndexes(int shift, IndexPredicate predicate) { |
||||
if (predicate == null || predicate.execute(index)) { |
||||
index += shift; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Loads all points of the mesh that do not belong to any edge. |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @return a list of points |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with file reading occur |
||||
*/ |
||||
public static List<Point> loadAll(Structure meshStructure) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Loading all points that do not belong to any edge from mesh: {0}", meshStructure.getName()); |
||||
List<Point> result = new ArrayList<Point>(); |
||||
|
||||
Pointer pMEdge = (Pointer) meshStructure.getFieldValue("medge"); |
||||
if (pMEdge.isNotNull()) { |
||||
int count = ((Number) meshStructure.getFieldValue("totvert")).intValue(); |
||||
Set<Integer> unusedVertices = new HashSet<Integer>(count); |
||||
for (int i = 0; i < count; ++i) { |
||||
unusedVertices.add(i); |
||||
} |
||||
|
||||
List<Structure> edges = pMEdge.fetchData(); |
||||
for (Structure edge : edges) { |
||||
unusedVertices.remove(((Number) edge.getFieldValue("v1")).intValue()); |
||||
unusedVertices.remove(((Number) edge.getFieldValue("v2")).intValue()); |
||||
} |
||||
|
||||
for (Integer unusedIndex : unusedVertices) { |
||||
result.add(new Point(unusedIndex)); |
||||
} |
||||
} |
||||
LOGGER.log(Level.FINE, "Loaded {0} points.", result.size()); |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,767 @@ |
||||
package com.jme3.scene.plugins.blender.meshes; |
||||
|
||||
import java.nio.IntBuffer; |
||||
import java.nio.ShortBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.bounding.BoundingBox; |
||||
import com.jme3.bounding.BoundingVolume; |
||||
import com.jme3.material.Material; |
||||
import com.jme3.material.RenderState.FaceCullMode; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Geometry; |
||||
import com.jme3.scene.Mesh; |
||||
import com.jme3.scene.Mesh.Mode; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.VertexBuffer; |
||||
import com.jme3.scene.VertexBuffer.Type; |
||||
import com.jme3.scene.VertexBuffer.Usage; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialContext; |
||||
import com.jme3.scene.plugins.blender.meshes.Face.TriangulationWarning; |
||||
import com.jme3.scene.plugins.blender.meshes.MeshBuffers.BoneBuffersData; |
||||
import com.jme3.scene.plugins.blender.modifiers.Modifier; |
||||
import com.jme3.scene.plugins.blender.objects.Properties; |
||||
|
||||
/** |
||||
* The class extends Geometry so that it can be temporalily added to the object's node. |
||||
* Later each such node's child will be transformed into a list of geometries. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class TemporalMesh extends Geometry { |
||||
private static final Logger LOGGER = Logger.getLogger(TemporalMesh.class.getName()); |
||||
/** A minimum weight value. */ |
||||
private static final double MINIMUM_BONE_WEIGHT = FastMath.DBL_EPSILON; |
||||
|
||||
/** The blender context. */ |
||||
protected final BlenderContext blenderContext; |
||||
|
||||
/** The mesh's structure. */ |
||||
protected final Structure meshStructure; |
||||
|
||||
/** Loaded vertices. */ |
||||
protected List<Vector3f> vertices = new ArrayList<Vector3f>(); |
||||
/** Loaded normals. */ |
||||
protected List<Vector3f> normals = new ArrayList<Vector3f>(); |
||||
/** Loaded vertex groups. */ |
||||
protected List<Map<String, Float>> vertexGroups = new ArrayList<Map<String, Float>>(); |
||||
/** Loaded vertex colors. */ |
||||
protected List<byte[]> verticesColors = new ArrayList<byte[]>(); |
||||
|
||||
/** Materials used by the mesh. */ |
||||
protected MaterialContext[] materials; |
||||
/** The properties of the mesh. */ |
||||
protected Properties properties; |
||||
/** The bone indexes. */ |
||||
protected Map<String, Integer> boneIndexes = new HashMap<String, Integer>(); |
||||
/** The modifiers that should be applied after the mesh has been created. */ |
||||
protected List<Modifier> postMeshCreationModifiers = new ArrayList<Modifier>(); |
||||
|
||||
/** The faces of the mesh. */ |
||||
protected List<Face> faces = new ArrayList<Face>(); |
||||
/** The edges of the mesh. */ |
||||
protected List<Edge> edges = new ArrayList<Edge>(); |
||||
/** The points of the mesh. */ |
||||
protected List<Point> points = new ArrayList<Point>(); |
||||
/** A map between index and faces that contain it (for faster index - face queries). */ |
||||
protected Map<Integer, Set<Face>> indexToFaceMapping = new HashMap<Integer, Set<Face>>(); |
||||
/** A map between index and edges that contain it (for faster index - edge queries). */ |
||||
protected Map<Integer, Set<Edge>> indexToEdgeMapping = new HashMap<Integer, Set<Edge>>(); |
||||
|
||||
/** The bounding box of the temporal mesh. */ |
||||
protected BoundingBox boundingBox; |
||||
|
||||
/** |
||||
* Creates a temporal mesh based on the given mesh structure. |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with file reading occur |
||||
*/ |
||||
public TemporalMesh(Structure meshStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
this(meshStructure, blenderContext, true); |
||||
} |
||||
|
||||
/** |
||||
* Creates a temporal mesh based on the given mesh structure. |
||||
* @param meshStructure |
||||
* the mesh structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @param loadData |
||||
* tells if the data should be loaded from the mesh structure |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with file reading occur |
||||
*/ |
||||
protected TemporalMesh(Structure meshStructure, BlenderContext blenderContext, boolean loadData) throws BlenderFileException { |
||||
this.blenderContext = blenderContext; |
||||
this.meshStructure = meshStructure; |
||||
|
||||
if (loadData) { |
||||
name = meshStructure.getName(); |
||||
|
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
|
||||
meshHelper.loadVerticesAndNormals(meshStructure, vertices, normals); |
||||
verticesColors = meshHelper.loadVerticesColors(meshStructure, blenderContext); |
||||
LinkedHashMap<String, List<Vector2f>> userUVGroups = meshHelper.loadUVCoordinates(meshStructure); |
||||
vertexGroups = meshHelper.loadVerticesGroups(meshStructure); |
||||
|
||||
faces = Face.loadAll(meshStructure, userUVGroups, verticesColors, this, blenderContext); |
||||
edges = Edge.loadAll(meshStructure, this); |
||||
points = Point.loadAll(meshStructure); |
||||
|
||||
this.rebuildIndexesMappings(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return the blender context |
||||
*/ |
||||
public BlenderContext getBlenderContext() { |
||||
return blenderContext; |
||||
} |
||||
|
||||
/** |
||||
* @return the vertices of the mesh |
||||
*/ |
||||
public List<Vector3f> getVertices() { |
||||
return vertices; |
||||
} |
||||
|
||||
/** |
||||
* @return the normals of the mesh |
||||
*/ |
||||
public List<Vector3f> getNormals() { |
||||
return normals; |
||||
} |
||||
|
||||
/** |
||||
* @return all faces |
||||
*/ |
||||
public List<Face> getFaces() { |
||||
return faces; |
||||
} |
||||
|
||||
/** |
||||
* @return all edges |
||||
*/ |
||||
public List<Edge> getEdges() { |
||||
return edges; |
||||
} |
||||
|
||||
/** |
||||
* @return all points (do not mistake it with vertices) |
||||
*/ |
||||
public List<Point> getPoints() { |
||||
return points; |
||||
} |
||||
|
||||
/** |
||||
* @return all vertices colors |
||||
*/ |
||||
public List<byte[]> getVerticesColors() { |
||||
return verticesColors; |
||||
} |
||||
|
||||
/** |
||||
* @return all vertex groups for the vertices (each map has groups for the proper vertex) |
||||
*/ |
||||
public List<Map<String, Float>> getVertexGroups() { |
||||
return vertexGroups; |
||||
} |
||||
|
||||
/** |
||||
* @return the faces that contain the given index or null if none contain it |
||||
*/ |
||||
public Collection<Face> getAdjacentFaces(Integer index) { |
||||
return indexToFaceMapping.get(index); |
||||
} |
||||
|
||||
/** |
||||
* @param edge the edge of the mesh |
||||
* @return a list of faces that contain the given edge or an empty list |
||||
*/ |
||||
public Collection<Face> getAdjacentFaces(Edge edge) { |
||||
List<Face> result = new ArrayList<Face>(indexToFaceMapping.get(edge.getFirstIndex())); |
||||
Set<Face> secondIndexAdjacentFaces = indexToFaceMapping.get(edge.getSecondIndex()); |
||||
if (secondIndexAdjacentFaces != null) { |
||||
result.retainAll(indexToFaceMapping.get(edge.getSecondIndex())); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @param index the index of the mesh |
||||
* @return a list of edges that contain the index |
||||
*/ |
||||
public Collection<Edge> getAdjacentEdges(Integer index) { |
||||
return indexToEdgeMapping.get(index); |
||||
} |
||||
|
||||
/** |
||||
* Tells if the given edge is a boundary edge. The boundary edge means that it belongs to a single |
||||
* face or to none. |
||||
* @param edge the edge of the mesh |
||||
* @return <b>true</b> if the edge is a boundary one and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isBoundary(Edge edge) { |
||||
return this.getAdjacentFaces(edge).size() <= 1; |
||||
} |
||||
|
||||
/** |
||||
* The method tells if the given index is a boundary index. A boundary index belongs to at least |
||||
* one boundary edge. |
||||
* @param index |
||||
* the index of the mesh |
||||
* @return <b>true</b> if the index is a boundary one and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isBoundary(Integer index) { |
||||
Collection<Edge> adjacentEdges = this.getAdjacentEdges(index); |
||||
for (Edge edge : adjacentEdges) { |
||||
if (this.isBoundary(edge)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public TemporalMesh clone() { |
||||
try { |
||||
TemporalMesh result = new TemporalMesh(meshStructure, blenderContext, false); |
||||
result.name = name; |
||||
for (Vector3f v : vertices) { |
||||
result.vertices.add(v.clone()); |
||||
} |
||||
for (Vector3f n : normals) { |
||||
result.normals.add(n.clone()); |
||||
} |
||||
for (Map<String, Float> group : vertexGroups) { |
||||
result.vertexGroups.add(new HashMap<String, Float>(group)); |
||||
} |
||||
for (byte[] vertColor : verticesColors) { |
||||
result.verticesColors.add(vertColor.clone()); |
||||
} |
||||
result.materials = materials; |
||||
result.properties = properties; |
||||
result.boneIndexes.putAll(boneIndexes); |
||||
result.postMeshCreationModifiers.addAll(postMeshCreationModifiers); |
||||
for (Face face : faces) { |
||||
result.faces.add(face.clone()); |
||||
} |
||||
for (Edge edge : edges) { |
||||
result.edges.add(edge.clone()); |
||||
} |
||||
for (Point point : points) { |
||||
result.points.add(point.clone()); |
||||
} |
||||
result.rebuildIndexesMappings(); |
||||
return result; |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.SEVERE, "Error while cloning the temporal mesh: {0}. Returning null.", e.getLocalizedMessage()); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* The method rebuilds the mappings between faces and edges. Should be called after |
||||
* every major change of the temporal mesh done outside it. |
||||
* <p> |
||||
* Note: I will remove this method soon and cause the mappings to be done |
||||
* automatically when the mesh is modified. |
||||
*/ |
||||
public void rebuildIndexesMappings() { |
||||
indexToEdgeMapping.clear(); |
||||
indexToFaceMapping.clear(); |
||||
for (Face face : faces) { |
||||
for (Integer index : face.getIndexes()) { |
||||
Set<Face> faces = indexToFaceMapping.get(index); |
||||
if (faces == null) { |
||||
faces = new HashSet<Face>(); |
||||
indexToFaceMapping.put(index, faces); |
||||
} |
||||
faces.add(face); |
||||
} |
||||
} |
||||
for (Edge edge : edges) { |
||||
Set<Edge> edges = indexToEdgeMapping.get(edge.getFirstIndex()); |
||||
if (edges == null) { |
||||
edges = new HashSet<Edge>(); |
||||
indexToEdgeMapping.put(edge.getFirstIndex(), edges); |
||||
} |
||||
edges.add(edge); |
||||
edges = indexToEdgeMapping.get(edge.getSecondIndex()); |
||||
if (edges == null) { |
||||
edges = new HashSet<Edge>(); |
||||
indexToEdgeMapping.put(edge.getSecondIndex(), edges); |
||||
} |
||||
edges.add(edge); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void updateModelBound() { |
||||
if (boundingBox == null) { |
||||
boundingBox = new BoundingBox(); |
||||
} |
||||
Vector3f min = new Vector3f(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE); |
||||
Vector3f max = new Vector3f(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); |
||||
for (Vector3f v : vertices) { |
||||
min.set(Math.min(min.x, v.x), Math.min(min.y, v.y), Math.min(min.z, v.z)); |
||||
max.set(Math.max(max.x, v.x), Math.max(max.y, v.y), Math.max(max.z, v.z)); |
||||
} |
||||
boundingBox.setMinMax(min, max); |
||||
} |
||||
|
||||
@Override |
||||
public BoundingVolume getModelBound() { |
||||
this.updateModelBound(); |
||||
return boundingBox; |
||||
} |
||||
|
||||
@Override |
||||
public BoundingVolume getWorldBound() { |
||||
this.updateModelBound(); |
||||
Node parent = this.getParent(); |
||||
if (parent != null) { |
||||
BoundingVolume bv = boundingBox.clone(); |
||||
bv.setCenter(parent.getWorldTranslation()); |
||||
return bv; |
||||
} else { |
||||
return boundingBox; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Triangulates the mesh. |
||||
*/ |
||||
public void triangulate() { |
||||
Set<TriangulationWarning> warnings = new HashSet<>(TriangulationWarning.values().length - 1); |
||||
LOGGER.fine("Triangulating temporal mesh."); |
||||
for (Face face : faces) { |
||||
TriangulationWarning warning = face.triangulate(); |
||||
if(warning != TriangulationWarning.NONE) { |
||||
warnings.add(warning); |
||||
} |
||||
} |
||||
|
||||
if(warnings.size() > 0 && LOGGER.isLoggable(Level.WARNING)) { |
||||
StringBuilder sb = new StringBuilder(512); |
||||
sb.append("There were problems with triangulating the faces of a mesh: ").append(name); |
||||
for(TriangulationWarning w : warnings) { |
||||
sb.append("\n\t").append(w); |
||||
} |
||||
LOGGER.warning(sb.toString()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method appends the given mesh to the current one. New faces and vertices and indexes are added. |
||||
* @param mesh |
||||
* the mesh to be appended |
||||
*/ |
||||
public void append(TemporalMesh mesh) { |
||||
if (mesh != null) { |
||||
// we need to shift the indexes in faces, lines and points
|
||||
int shift = vertices.size(); |
||||
if (shift > 0) { |
||||
for (Face face : mesh.faces) { |
||||
face.getIndexes().shiftIndexes(shift, null); |
||||
face.setTemporalMesh(this); |
||||
} |
||||
for (Edge edge : mesh.edges) { |
||||
edge.shiftIndexes(shift, null); |
||||
} |
||||
for (Point point : mesh.points) { |
||||
point.shiftIndexes(shift, null); |
||||
} |
||||
} |
||||
|
||||
faces.addAll(mesh.faces); |
||||
edges.addAll(mesh.edges); |
||||
points.addAll(mesh.points); |
||||
|
||||
vertices.addAll(mesh.vertices); |
||||
normals.addAll(mesh.normals); |
||||
vertexGroups.addAll(mesh.vertexGroups); |
||||
verticesColors.addAll(mesh.verticesColors); |
||||
boneIndexes.putAll(mesh.boneIndexes); |
||||
|
||||
this.rebuildIndexesMappings(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets the properties of the mesh. |
||||
* @param properties |
||||
* the properties of the mesh |
||||
*/ |
||||
public void setProperties(Properties properties) { |
||||
this.properties = properties; |
||||
} |
||||
|
||||
/** |
||||
* Sets the materials of the mesh. |
||||
* @param materials |
||||
* the materials of the mesh |
||||
*/ |
||||
public void setMaterials(MaterialContext[] materials) { |
||||
this.materials = materials; |
||||
} |
||||
|
||||
/** |
||||
* Adds bone index to the mesh. |
||||
* @param boneName |
||||
* the name of the bone |
||||
* @param boneIndex |
||||
* the index of the bone |
||||
*/ |
||||
public void addBoneIndex(String boneName, Integer boneIndex) { |
||||
boneIndexes.put(boneName, boneIndex); |
||||
} |
||||
|
||||
/** |
||||
* The modifier to be applied after the geometries are created. |
||||
* @param modifier |
||||
* the modifier to be applied |
||||
*/ |
||||
public void applyAfterMeshCreate(Modifier modifier) { |
||||
postMeshCreationModifiers.add(modifier); |
||||
} |
||||
|
||||
@Override |
||||
public int getVertexCount() { |
||||
return vertices.size(); |
||||
} |
||||
|
||||
/** |
||||
* Removes all vertices from the mesh. |
||||
*/ |
||||
public void clear() { |
||||
vertices.clear(); |
||||
normals.clear(); |
||||
vertexGroups.clear(); |
||||
verticesColors.clear(); |
||||
faces.clear(); |
||||
edges.clear(); |
||||
points.clear(); |
||||
indexToEdgeMapping.clear(); |
||||
indexToFaceMapping.clear(); |
||||
} |
||||
|
||||
/** |
||||
* The mesh builds geometries from the mesh. The result is stored in the blender context |
||||
* under the mesh's OMA. |
||||
*/ |
||||
public void toGeometries() { |
||||
LOGGER.log(Level.FINE, "Converting temporal mesh {0} to jme geometries.", name); |
||||
List<Geometry> result = new ArrayList<Geometry>(); |
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
Node parent = this.getParent(); |
||||
parent.detachChild(this); |
||||
|
||||
this.prepareFacesGeometry(result, meshHelper); |
||||
this.prepareLinesGeometry(result, meshHelper); |
||||
this.preparePointsGeometry(result, meshHelper); |
||||
|
||||
blenderContext.addLoadedFeatures(meshStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result); |
||||
|
||||
for (Geometry geometry : result) { |
||||
parent.attachChild(geometry); |
||||
} |
||||
|
||||
for (Modifier modifier : postMeshCreationModifiers) { |
||||
modifier.postMeshCreationApply(parent, blenderContext); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method creates geometries from faces. |
||||
* @param result |
||||
* the list where new geometries will be appended |
||||
* @param meshHelper |
||||
* the mesh helper |
||||
*/ |
||||
protected void prepareFacesGeometry(List<Geometry> result, MeshHelper meshHelper) { |
||||
LOGGER.fine("Preparing faces geometries."); |
||||
this.triangulate(); |
||||
|
||||
Vector3f[] tempVerts = new Vector3f[3]; |
||||
Vector3f[] tempNormals = new Vector3f[3]; |
||||
byte[][] tempVertColors = new byte[3][]; |
||||
List<Map<Float, Integer>> boneBuffers = new ArrayList<Map<Float, Integer>>(3); |
||||
|
||||
LOGGER.log(Level.FINE, "Appending {0} faces to mesh buffers.", faces.size()); |
||||
Map<Integer, MeshBuffers> faceMeshes = new HashMap<Integer, MeshBuffers>(); |
||||
for (Face face : faces) { |
||||
MeshBuffers meshBuffers = faceMeshes.get(face.getMaterialNumber()); |
||||
if (meshBuffers == null) { |
||||
meshBuffers = new MeshBuffers(face.getMaterialNumber()); |
||||
faceMeshes.put(face.getMaterialNumber(), meshBuffers); |
||||
} |
||||
|
||||
List<List<Integer>> triangulatedIndexes = face.getCurrentIndexes(); |
||||
List<byte[]> vertexColors = face.getVertexColors(); |
||||
|
||||
for (List<Integer> indexes : triangulatedIndexes) { |
||||
assert indexes.size() == 3 : "The mesh has not been properly triangulated!"; |
||||
|
||||
Vector3f normal = null; |
||||
if(!face.isSmooth()) { |
||||
normal = FastMath.computeNormal(vertices.get(indexes.get(0)), vertices.get(indexes.get(1)), vertices.get(indexes.get(2))); |
||||
} |
||||
|
||||
boneBuffers.clear(); |
||||
for (int i = 0; i < 3; ++i) { |
||||
int vertIndex = indexes.get(i); |
||||
tempVerts[i] = vertices.get(vertIndex); |
||||
tempNormals[i] = normal != null ? normal : normals.get(vertIndex); |
||||
tempVertColors[i] = vertexColors != null ? vertexColors.get(face.getIndexes().indexOf(vertIndex)) : null; |
||||
|
||||
if (boneIndexes.size() > 0 && vertexGroups.size() > 0) { |
||||
Map<Float, Integer> boneBuffersForVertex = new HashMap<Float, Integer>(); |
||||
Map<String, Float> vertexGroupsForVertex = vertexGroups.get(vertIndex); |
||||
for (Entry<String, Integer> entry : boneIndexes.entrySet()) { |
||||
if (vertexGroupsForVertex.containsKey(entry.getKey())) { |
||||
float weight = vertexGroupsForVertex.get(entry.getKey()); |
||||
if (weight > MINIMUM_BONE_WEIGHT) { |
||||
// only values of weight greater than MINIMUM_BONE_WEIGHT are used
|
||||
// if all non zero weights were used, and they were samm enough, problems with normalisation would occur
|
||||
// because adding a very small value to 1.0 will give 1.0
|
||||
// so in order to avoid such errors, which can cause severe animation artifacts we need to use some minimum weight value
|
||||
boneBuffersForVertex.put(weight, entry.getValue()); |
||||
} |
||||
} |
||||
} |
||||
if (boneBuffersForVertex.size() == 0) {// attach the vertex to zero-indexed bone so that it does not collapse to (0, 0, 0)
|
||||
boneBuffersForVertex.put(1.0f, 0); |
||||
} |
||||
boneBuffers.add(boneBuffersForVertex); |
||||
} |
||||
} |
||||
|
||||
Map<String, List<Vector2f>> uvs = meshHelper.selectUVSubset(face, indexes.toArray(new Integer[indexes.size()])); |
||||
meshBuffers.append(face.isSmooth(), tempVerts, tempNormals, uvs, tempVertColors, boneBuffers); |
||||
} |
||||
} |
||||
|
||||
LOGGER.fine("Converting mesh buffers to geometries."); |
||||
Map<Geometry, MeshBuffers> geometryToBuffersMap = new HashMap<Geometry, MeshBuffers>(); |
||||
for (Entry<Integer, MeshBuffers> entry : faceMeshes.entrySet()) { |
||||
MeshBuffers meshBuffers = entry.getValue(); |
||||
|
||||
Mesh mesh = new Mesh(); |
||||
|
||||
if (meshBuffers.isShortIndexBuffer()) { |
||||
mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer()); |
||||
} else { |
||||
mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer()); |
||||
} |
||||
mesh.setBuffer(meshBuffers.getPositionsBuffer()); |
||||
mesh.setBuffer(meshBuffers.getNormalsBuffer()); |
||||
if (meshBuffers.areVertexColorsUsed()) { |
||||
mesh.setBuffer(Type.Color, 4, meshBuffers.getVertexColorsBuffer()); |
||||
mesh.getBuffer(Type.Color).setNormalized(true); |
||||
} |
||||
|
||||
BoneBuffersData boneBuffersData = meshBuffers.getBoneBuffers(); |
||||
if (boneBuffersData != null) { |
||||
mesh.setMaxNumWeights(boneBuffersData.maximumWeightsPerVertex); |
||||
mesh.setBuffer(boneBuffersData.verticesWeights); |
||||
mesh.setBuffer(boneBuffersData.verticesWeightsIndices); |
||||
|
||||
LOGGER.fine("Generating bind pose and normal buffers."); |
||||
mesh.generateBindPose(true); |
||||
|
||||
// change the usage type of vertex and normal buffers from Static to Stream
|
||||
mesh.getBuffer(Type.Position).setUsage(Usage.Stream); |
||||
mesh.getBuffer(Type.Normal).setUsage(Usage.Stream); |
||||
|
||||
// creating empty buffers for HW skinning; the buffers will be setup if ever used
|
||||
VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight); |
||||
VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex); |
||||
mesh.setBuffer(verticesWeightsHW); |
||||
mesh.setBuffer(verticesWeightsIndicesHW); |
||||
} |
||||
|
||||
Geometry geometry = new Geometry(name + (result.size() + 1), mesh); |
||||
if (properties != null && properties.getValue() != null) { |
||||
meshHelper.applyProperties(geometry, properties); |
||||
} |
||||
result.add(geometry); |
||||
|
||||
geometryToBuffersMap.put(geometry, meshBuffers); |
||||
} |
||||
|
||||
LOGGER.fine("Applying materials to geometries."); |
||||
for (Entry<Geometry, MeshBuffers> entry : geometryToBuffersMap.entrySet()) { |
||||
int materialIndex = entry.getValue().getMaterialIndex(); |
||||
Geometry geometry = entry.getKey(); |
||||
if (materialIndex >= 0 && materials != null && materials.length > materialIndex && materials[materialIndex] != null) { |
||||
materials[materialIndex].applyMaterial(geometry, meshStructure.getOldMemoryAddress(), entry.getValue().getUvCoords(), blenderContext); |
||||
} else { |
||||
Material defaultMaterial = blenderContext.getDefaultMaterial().clone(); |
||||
defaultMaterial.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off); |
||||
geometry.setMaterial(defaultMaterial); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method creates geometries from lines. |
||||
* @param result |
||||
* the list where new geometries will be appended |
||||
* @param meshHelper |
||||
* the mesh helper |
||||
*/ |
||||
protected void prepareLinesGeometry(List<Geometry> result, MeshHelper meshHelper) { |
||||
if (edges.size() > 0) { |
||||
LOGGER.fine("Preparing lines geometries."); |
||||
|
||||
List<List<Integer>> separateEdges = new ArrayList<List<Integer>>(); |
||||
List<Edge> edges = new ArrayList<Edge>(this.edges.size()); |
||||
for (Edge edge : this.edges) { |
||||
if (!edge.isInFace()) { |
||||
edges.add(edge); |
||||
} |
||||
} |
||||
while (edges.size() > 0) { |
||||
boolean edgeAppended = false; |
||||
int edgeIndex = 0; |
||||
for (List<Integer> list : separateEdges) { |
||||
for (edgeIndex = 0; edgeIndex < edges.size() && !edgeAppended; ++edgeIndex) { |
||||
Edge edge = edges.get(edgeIndex); |
||||
if (list.get(0).equals(edge.getFirstIndex())) { |
||||
list.add(0, edge.getSecondIndex()); |
||||
--edgeIndex; |
||||
edgeAppended = true; |
||||
} else if (list.get(0).equals(edge.getSecondIndex())) { |
||||
list.add(0, edge.getFirstIndex()); |
||||
--edgeIndex; |
||||
edgeAppended = true; |
||||
} else if (list.get(list.size() - 1).equals(edge.getFirstIndex())) { |
||||
list.add(edge.getSecondIndex()); |
||||
--edgeIndex; |
||||
edgeAppended = true; |
||||
} else if (list.get(list.size() - 1).equals(edge.getSecondIndex())) { |
||||
list.add(edge.getFirstIndex()); |
||||
--edgeIndex; |
||||
edgeAppended = true; |
||||
} |
||||
} |
||||
if (edgeAppended) { |
||||
break; |
||||
} |
||||
} |
||||
Edge edge = edges.remove(edgeAppended ? edgeIndex : 0); |
||||
if (!edgeAppended) { |
||||
separateEdges.add(new ArrayList<Integer>(Arrays.asList(edge.getFirstIndex(), edge.getSecondIndex()))); |
||||
} |
||||
} |
||||
|
||||
for (List<Integer> list : separateEdges) { |
||||
MeshBuffers meshBuffers = new MeshBuffers(0); |
||||
for (int index : list) { |
||||
meshBuffers.append(vertices.get(index), normals.get(index)); |
||||
} |
||||
Mesh mesh = new Mesh(); |
||||
mesh.setLineWidth(blenderContext.getBlenderKey().getLinesWidth()); |
||||
mesh.setMode(Mode.LineStrip); |
||||
if (meshBuffers.isShortIndexBuffer()) { |
||||
mesh.setBuffer(Type.Index, 1, (ShortBuffer) meshBuffers.getIndexBuffer()); |
||||
} else { |
||||
mesh.setBuffer(Type.Index, 1, (IntBuffer) meshBuffers.getIndexBuffer()); |
||||
} |
||||
mesh.setBuffer(meshBuffers.getPositionsBuffer()); |
||||
mesh.setBuffer(meshBuffers.getNormalsBuffer()); |
||||
|
||||
Geometry geometry = new Geometry(meshStructure.getName() + (result.size() + 1), mesh); |
||||
geometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext)); |
||||
if (properties != null && properties.getValue() != null) { |
||||
meshHelper.applyProperties(geometry, properties); |
||||
} |
||||
result.add(geometry); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method creates geometries from points. |
||||
* @param result |
||||
* the list where new geometries will be appended |
||||
* @param meshHelper |
||||
* the mesh helper |
||||
*/ |
||||
protected void preparePointsGeometry(List<Geometry> result, MeshHelper meshHelper) { |
||||
if (points.size() > 0) { |
||||
LOGGER.fine("Preparing point geometries."); |
||||
|
||||
MeshBuffers pointBuffers = new MeshBuffers(0); |
||||
for (Point point : points) { |
||||
pointBuffers.append(vertices.get(point.getIndex()), normals.get(point.getIndex())); |
||||
} |
||||
Mesh pointsMesh = new Mesh(); |
||||
pointsMesh.setMode(Mode.Points); |
||||
pointsMesh.setPointSize(blenderContext.getBlenderKey().getPointsSize()); |
||||
if (pointBuffers.isShortIndexBuffer()) { |
||||
pointsMesh.setBuffer(Type.Index, 1, (ShortBuffer) pointBuffers.getIndexBuffer()); |
||||
} else { |
||||
pointsMesh.setBuffer(Type.Index, 1, (IntBuffer) pointBuffers.getIndexBuffer()); |
||||
} |
||||
pointsMesh.setBuffer(pointBuffers.getPositionsBuffer()); |
||||
pointsMesh.setBuffer(pointBuffers.getNormalsBuffer()); |
||||
|
||||
Geometry pointsGeometry = new Geometry(meshStructure.getName() + (result.size() + 1), pointsMesh); |
||||
pointsGeometry.setMaterial(meshHelper.getBlackUnshadedMaterial(blenderContext)); |
||||
if (properties != null && properties.getValue() != null) { |
||||
meshHelper.applyProperties(pointsGeometry, properties); |
||||
} |
||||
result.add(pointsGeometry); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "TemporalMesh [name=" + name + ", vertices.size()=" + vertices.size() + "]"; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
final int prime = 31; |
||||
int result = 1; |
||||
result = prime * result + (meshStructure == null ? 0 : meshStructure.hashCode()); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (!(obj instanceof TemporalMesh)) { |
||||
return false; |
||||
} |
||||
TemporalMesh other = (TemporalMesh) obj; |
||||
return meshStructure.getOldMemoryAddress().equals(other.meshStructure.getOldMemoryAddress()); |
||||
} |
||||
} |
@ -0,0 +1,113 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.AnimationHelper; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
|
||||
/** |
||||
* This modifier allows to add bone animation to the object. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ArmatureModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(ArmatureModifier.class.getName()); |
||||
|
||||
private static final int FLAG_VERTEX_GROUPS = 0x01; |
||||
private static final int FLAG_BONE_ENVELOPES = 0x02; |
||||
|
||||
private Skeleton skeleton; |
||||
|
||||
/** |
||||
* This constructor reads animation data from the object structore. The |
||||
* stored data is the AnimData and additional data is armature's OMA. |
||||
* |
||||
* @param objectStructure |
||||
* the structure of the object |
||||
* @param modifierStructure |
||||
* the structure of the modifier |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public ArmatureModifier(Structure objectStructure, Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
Pointer pArmatureObject = (Pointer) modifierStructure.getFieldValue("object"); |
||||
if (pArmatureObject.isNotNull()) { |
||||
int deformflag = ((Number) modifierStructure.getFieldValue("deformflag")).intValue(); |
||||
boolean useVertexGroups = (deformflag & FLAG_VERTEX_GROUPS) != 0; |
||||
boolean useBoneEnvelopes = (deformflag & FLAG_BONE_ENVELOPES) != 0; |
||||
modifying = useBoneEnvelopes || useVertexGroups; |
||||
if (modifying) {// if neither option is used the modifier will not modify anything anyway
|
||||
Structure armatureObject = pArmatureObject.fetchData().get(0); |
||||
if(blenderContext.getSkeleton(armatureObject.getOldMemoryAddress()) == null) { |
||||
LOGGER.fine("Creating new skeleton for armature modifier."); |
||||
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData().get(0); |
||||
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(); |
||||
List<Bone> bonesList = new ArrayList<Bone>(); |
||||
for (int i = 0; i < bonebase.size(); ++i) { |
||||
this.buildBones(armatureObject.getOldMemoryAddress(), bonebase.get(i), null, bonesList, objectStructure.getOldMemoryAddress(), blenderContext); |
||||
} |
||||
bonesList.add(0, new Bone("")); |
||||
Bone[] bones = bonesList.toArray(new Bone[bonesList.size()]); |
||||
skeleton = new Skeleton(bones); |
||||
blenderContext.setSkeleton(armatureObject.getOldMemoryAddress(), skeleton); |
||||
} else { |
||||
skeleton = blenderContext.getSkeleton(armatureObject.getOldMemoryAddress()); |
||||
} |
||||
} |
||||
} else { |
||||
modifying = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void buildBones(Long armatureObjectOMA, Structure boneStructure, Bone parent, List<Bone> result, Long spatialOMA, BlenderContext blenderContext) throws BlenderFileException { |
||||
BoneContext bc = new BoneContext(armatureObjectOMA, boneStructure, blenderContext); |
||||
bc.buildBone(result, spatialOMA, blenderContext); |
||||
} |
||||
|
||||
@Override |
||||
public void postMeshCreationApply(Node node, BlenderContext blenderContext) { |
||||
LOGGER.fine("Applying armature modifier after mesh has been created."); |
||||
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); |
||||
animationHelper.applyAnimations(node, skeleton, blenderContext.getBlenderKey().getAnimationMatchMethod()); |
||||
node.updateModelBound(); |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Armature modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} |
||||
|
||||
if (modifying) { |
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if (temporalMesh != null) { |
||||
LOGGER.log(Level.FINE, "Applying armature modifier to: {0}", temporalMesh); |
||||
|
||||
LOGGER.fine("Creating map between bone name and its index."); |
||||
for (int i = 0; i < skeleton.getBoneCount(); ++i) { |
||||
Bone bone = skeleton.getBone(i); |
||||
temporalMesh.addBoneIndex(bone.getName(), i); |
||||
} |
||||
temporalMesh.applyAfterMeshCreate(this); |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,241 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.bounding.BoundingBox; |
||||
import com.jme3.bounding.BoundingSphere; |
||||
import com.jme3.bounding.BoundingVolume; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Geometry; |
||||
import com.jme3.scene.Mesh; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.MeshHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
import com.jme3.scene.shape.Curve; |
||||
|
||||
/** |
||||
* This modifier allows to array modifier to the object. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ArrayModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(ArrayModifier.class.getName()); |
||||
|
||||
private int fittype; |
||||
private int count; |
||||
private float length; |
||||
private float[] offset; |
||||
private float[] scale; |
||||
private Pointer pOffsetObject; |
||||
private Pointer pStartCap; |
||||
private Pointer pEndCap; |
||||
|
||||
/** |
||||
* This constructor reads array data from the modifier structure. The |
||||
* stored data is a map of parameters for array modifier. No additional data |
||||
* is loaded. |
||||
* |
||||
* @param objectStructure |
||||
* the structure of the object |
||||
* @param modifierStructure |
||||
* the structure of the modifier |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public ArrayModifier(Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
fittype = ((Number) modifierStructure.getFieldValue("fit_type")).intValue(); |
||||
switch (fittype) { |
||||
case 0:// FIXED COUNT
|
||||
count = ((Number) modifierStructure.getFieldValue("count")).intValue(); |
||||
break; |
||||
case 1:// FIXED LENGTH
|
||||
length = ((Number) modifierStructure.getFieldValue("length")).floatValue(); |
||||
break; |
||||
case 2:// FITCURVE
|
||||
Pointer pCurveOb = (Pointer) modifierStructure.getFieldValue("curve_ob"); |
||||
if (pCurveOb.isNotNull()) { |
||||
Structure curveStructure = pCurveOb.fetchData().get(0); |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
Node curveObject = (Node) objectHelper.toObject(curveStructure, blenderContext); |
||||
Set<Number> referencesToCurveLengths = new HashSet<Number>(curveObject.getChildren().size()); |
||||
for (Spatial spatial : curveObject.getChildren()) { |
||||
if (spatial instanceof Geometry) { |
||||
Mesh mesh = ((Geometry) spatial).getMesh(); |
||||
if (mesh instanceof Curve) { |
||||
length += ((Curve) mesh).getLength(); |
||||
} else { |
||||
// if bevel object has several parts then each mesh will have the same reference
|
||||
// to length value (and we should use only one)
|
||||
Number curveLength = spatial.getUserData("curveLength"); |
||||
if (curveLength != null && !referencesToCurveLengths.contains(curveLength)) { |
||||
length += curveLength.floatValue(); |
||||
referencesToCurveLengths.add(curveLength); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
fittype = 1;// treat it like FIXED LENGTH
|
||||
break; |
||||
default: |
||||
assert false : "Unknown array modifier fit type: " + fittype; |
||||
} |
||||
|
||||
// offset parameters
|
||||
int offsettype = ((Number) modifierStructure.getFieldValue("offset_type")).intValue(); |
||||
if ((offsettype & 0x01) != 0) {// Constant offset
|
||||
DynamicArray<Number> offsetArray = (DynamicArray<Number>) modifierStructure.getFieldValue("offset"); |
||||
offset = new float[] { offsetArray.get(0).floatValue(), offsetArray.get(1).floatValue(), offsetArray.get(2).floatValue() }; |
||||
} |
||||
if ((offsettype & 0x02) != 0) {// Relative offset
|
||||
DynamicArray<Number> scaleArray = (DynamicArray<Number>) modifierStructure.getFieldValue("scale"); |
||||
scale = new float[] { scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue() }; |
||||
} |
||||
if ((offsettype & 0x04) != 0) {// Object offset
|
||||
pOffsetObject = (Pointer) modifierStructure.getFieldValue("offset_ob"); |
||||
} |
||||
|
||||
// start cap and end cap
|
||||
pStartCap = (Pointer) modifierStructure.getFieldValue("start_cap"); |
||||
pEndCap = (Pointer) modifierStructure.getFieldValue("end_cap"); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Array modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} else { |
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if (temporalMesh != null) { |
||||
LOGGER.log(Level.FINE, "Applying array modifier to: {0}", temporalMesh); |
||||
if (offset == null) {// the node will be repeated several times in the same place
|
||||
offset = new float[] { 0.0f, 0.0f, 0.0f }; |
||||
} |
||||
if (scale == null) {// the node will be repeated several times in the same place
|
||||
scale = new float[] { 0.0f, 0.0f, 0.0f }; |
||||
} else { |
||||
// getting bounding box
|
||||
temporalMesh.updateModelBound(); |
||||
BoundingVolume boundingVolume = temporalMesh.getWorldBound(); |
||||
if (boundingVolume instanceof BoundingBox) { |
||||
scale[0] *= ((BoundingBox) boundingVolume).getXExtent() * 2.0f; |
||||
scale[1] *= ((BoundingBox) boundingVolume).getYExtent() * 2.0f; |
||||
scale[2] *= ((BoundingBox) boundingVolume).getZExtent() * 2.0f; |
||||
} else if (boundingVolume instanceof BoundingSphere) { |
||||
float radius = ((BoundingSphere) boundingVolume).getRadius(); |
||||
scale[0] *= radius * 2.0f; |
||||
scale[1] *= radius * 2.0f; |
||||
scale[2] *= radius * 2.0f; |
||||
} else { |
||||
throw new IllegalStateException("Unknown bounding volume type: " + boundingVolume.getClass().getName()); |
||||
} |
||||
} |
||||
|
||||
// adding object's offset
|
||||
float[] objectOffset = new float[] { 0.0f, 0.0f, 0.0f }; |
||||
if (pOffsetObject != null && pOffsetObject.isNotNull()) { |
||||
FileBlockHeader offsetObjectBlock = blenderContext.getFileBlock(pOffsetObject.getOldMemoryAddress()); |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
try {// we take the structure in case the object was not yet loaded
|
||||
Structure offsetStructure = offsetObjectBlock.getStructure(blenderContext); |
||||
Vector3f translation = objectHelper.getTransformation(offsetStructure, blenderContext).getTranslation(); |
||||
objectOffset[0] = translation.x; |
||||
objectOffset[1] = translation.y; |
||||
objectOffset[2] = translation.z; |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.WARNING, "Problems in blender file structure! Object offset cannot be applied! The problem: {0}", e.getMessage()); |
||||
} |
||||
} |
||||
|
||||
// getting start and end caps
|
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
TemporalMesh[] caps = new TemporalMesh[] { null, null }; |
||||
Pointer[] pCaps = new Pointer[] { pStartCap, pEndCap }; |
||||
for (int i = 0; i < pCaps.length; ++i) { |
||||
if (pCaps[i].isNotNull()) { |
||||
FileBlockHeader capBlock = blenderContext.getFileBlock(pCaps[i].getOldMemoryAddress()); |
||||
try {// we take the structure in case the object was not yet loaded
|
||||
Structure capStructure = capBlock.getStructure(blenderContext); |
||||
Pointer pMesh = (Pointer) capStructure.getFieldValue("data"); |
||||
List<Structure> meshesArray = pMesh.fetchData(); |
||||
caps[i] = meshHelper.toTemporalMesh(meshesArray.get(0), blenderContext); |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.WARNING, "Problems in blender file structure! Cap object cannot be applied! The problem: {0}", e.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Vector3f translationVector = new Vector3f(offset[0] + scale[0] + objectOffset[0], offset[1] + scale[1] + objectOffset[1], offset[2] + scale[2] + objectOffset[2]); |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
float y = translationVector.y; |
||||
translationVector.y = translationVector.z; |
||||
translationVector.z = y == 0 ? 0 : -y; |
||||
} |
||||
|
||||
// getting/calculating repeats amount
|
||||
int count = 0; |
||||
if (fittype == 0) {// Fixed count
|
||||
count = this.count - 1; |
||||
} else if (fittype == 1) {// Fixed length
|
||||
float length = this.length; |
||||
if (translationVector.length() > 0.0f) { |
||||
count = (int) (length / translationVector.length()) - 1; |
||||
} |
||||
} else if (fittype == 2) {// Fit curve
|
||||
throw new IllegalStateException("Fit curve should be transformed to Fixed Length array type!"); |
||||
} else { |
||||
throw new IllegalStateException("Unknown fit type: " + fittype); |
||||
} |
||||
|
||||
// adding translated nodes and caps
|
||||
Vector3f totalTranslation = new Vector3f(translationVector); |
||||
if (count > 0) { |
||||
TemporalMesh originalMesh = temporalMesh.clone(); |
||||
for (int i = 0; i < count; ++i) { |
||||
TemporalMesh clone = originalMesh.clone(); |
||||
for (Vector3f v : clone.getVertices()) { |
||||
v.addLocal(totalTranslation); |
||||
} |
||||
temporalMesh.append(clone); |
||||
totalTranslation.addLocal(translationVector); |
||||
} |
||||
} |
||||
if (caps[0] != null) { |
||||
translationVector.multLocal(-1); |
||||
TemporalMesh capsClone = caps[0].clone(); |
||||
for (Vector3f v : capsClone.getVertices()) { |
||||
v.addLocal(translationVector); |
||||
} |
||||
temporalMesh.append(capsClone); |
||||
} |
||||
if (caps[1] != null) { |
||||
TemporalMesh capsClone = caps[1].clone(); |
||||
for (Vector3f v : capsClone.getVertices()) { |
||||
v.addLocal(totalTranslation); |
||||
} |
||||
temporalMesh.append(capsClone); |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,200 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.Edge; |
||||
import com.jme3.scene.plugins.blender.meshes.Face; |
||||
import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate; |
||||
import com.jme3.scene.plugins.blender.meshes.Point; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
|
||||
/** |
||||
* This modifier allows to use mask modifier on the object. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class MaskModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(MaskModifier.class.getName()); |
||||
|
||||
private static final int FLAG_INVERT_MASK = 0x01; |
||||
|
||||
private static final int MODE_VERTEX_GROUP = 0; |
||||
private static final int MODE_ARMATURE = 1; |
||||
|
||||
private Pointer pArmatureObject; |
||||
private String vertexGroupName; |
||||
private boolean invertMask; |
||||
|
||||
public MaskModifier(Structure modifierStructure, BlenderContext blenderContext) { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
int flag = ((Number) modifierStructure.getFieldValue("flag")).intValue(); |
||||
invertMask = (flag & FLAG_INVERT_MASK) != 0; |
||||
|
||||
int mode = ((Number) modifierStructure.getFieldValue("mode")).intValue(); |
||||
if (mode == MODE_VERTEX_GROUP) { |
||||
vertexGroupName = modifierStructure.getFieldValue("vgroup").toString(); |
||||
if (vertexGroupName != null && vertexGroupName.length() == 0) { |
||||
vertexGroupName = null; |
||||
} |
||||
} else if (mode == MODE_ARMATURE) { |
||||
pArmatureObject = (Pointer) modifierStructure.getFieldValue("ob_arm"); |
||||
} else { |
||||
LOGGER.log(Level.SEVERE, "Unknown mode type: {0}. Cannot apply modifier: {1}.", new Object[] { mode, modifierStructure.getName() }); |
||||
invalid = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Mirror modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} else { |
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if (temporalMesh != null) { |
||||
List<String> vertexGroupsToRemove = new ArrayList<String>(); |
||||
if (vertexGroupName != null) { |
||||
vertexGroupsToRemove.add(vertexGroupName); |
||||
} else if (pArmatureObject != null && pArmatureObject.isNotNull()) { |
||||
try { |
||||
Structure armatureObject = pArmatureObject.fetchData().get(0); |
||||
|
||||
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData().get(0); |
||||
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(); |
||||
vertexGroupsToRemove.addAll(this.readBoneNames(bonebase)); |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.SEVERE, "Cannot load armature object for the mask modifier. Cause: {0}", e.getLocalizedMessage()); |
||||
LOGGER.log(Level.SEVERE, "Mask modifier will NOT be applied to node named: {0}", node.getName()); |
||||
} |
||||
} else { |
||||
// if the mesh has no vertex groups then remove all verts
|
||||
// if the mesh has at least one vertex group - then do nothing
|
||||
// I have no idea why we should do that, but blender works this way
|
||||
Set<String> vertexGroupNames = new HashSet<String>(); |
||||
for (Map<String, Float> groups : temporalMesh.getVertexGroups()) { |
||||
vertexGroupNames.addAll(groups.keySet()); |
||||
} |
||||
if (vertexGroupNames.size() == 0 && !invertMask || vertexGroupNames.size() > 0 && invertMask) { |
||||
temporalMesh.clear(); |
||||
} |
||||
} |
||||
|
||||
if (vertexGroupsToRemove.size() > 0) { |
||||
List<Integer> vertsToBeRemoved = new ArrayList<Integer>(); |
||||
for (int i = 0; i < temporalMesh.getVertexCount(); ++i) { |
||||
Map<String, Float> vertexGroups = temporalMesh.getVertexGroups().get(i); |
||||
boolean hasVertexGroup = false; |
||||
if (vertexGroups != null) { |
||||
for (String groupName : vertexGroupsToRemove) { |
||||
Float weight = vertexGroups.get(groupName); |
||||
if (weight != null && weight > 0) { |
||||
hasVertexGroup = true; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (!hasVertexGroup && !invertMask || hasVertexGroup && invertMask) { |
||||
vertsToBeRemoved.add(i); |
||||
} |
||||
} |
||||
|
||||
Collections.reverse(vertsToBeRemoved); |
||||
for (Integer vertexIndex : vertsToBeRemoved) { |
||||
this.removeVertexAt(vertexIndex, temporalMesh); |
||||
} |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Every face, edge and point that contains |
||||
* the vertex will be removed. |
||||
* @param index |
||||
* the index of a vertex to be removed |
||||
* @throws IndexOutOfBoundsException |
||||
* thrown when given index is negative or beyond the count of vertices |
||||
*/ |
||||
private void removeVertexAt(final int index, TemporalMesh temporalMesh) { |
||||
if (index < 0 || index >= temporalMesh.getVertexCount()) { |
||||
throw new IndexOutOfBoundsException("The given index is out of bounds: " + index); |
||||
} |
||||
|
||||
temporalMesh.getVertices().remove(index); |
||||
temporalMesh.getNormals().remove(index); |
||||
if (temporalMesh.getVertexGroups().size() > 0) { |
||||
temporalMesh.getVertexGroups().remove(index); |
||||
} |
||||
if (temporalMesh.getVerticesColors().size() > 0) { |
||||
temporalMesh.getVerticesColors().remove(index); |
||||
} |
||||
|
||||
IndexPredicate shiftPredicate = new IndexPredicate() { |
||||
@Override |
||||
public boolean execute(Integer i) { |
||||
return i > index; |
||||
} |
||||
}; |
||||
for (int i = temporalMesh.getFaces().size() - 1; i >= 0; --i) { |
||||
Face face = temporalMesh.getFaces().get(i); |
||||
if (face.getIndexes().indexOf(index) >= 0) { |
||||
temporalMesh.getFaces().remove(i); |
||||
} else { |
||||
face.getIndexes().shiftIndexes(-1, shiftPredicate); |
||||
} |
||||
} |
||||
for (int i = temporalMesh.getEdges().size() - 1; i >= 0; --i) { |
||||
Edge edge = temporalMesh.getEdges().get(i); |
||||
if (edge.getFirstIndex() == index || edge.getSecondIndex() == index) { |
||||
temporalMesh.getEdges().remove(i); |
||||
} else { |
||||
edge.shiftIndexes(-1, shiftPredicate); |
||||
} |
||||
} |
||||
for (int i = temporalMesh.getPoints().size() - 1; i >= 0; --i) { |
||||
Point point = temporalMesh.getPoints().get(i); |
||||
if (point.getIndex() == index) { |
||||
temporalMesh.getPoints().remove(i); |
||||
} else { |
||||
point.shiftIndexes(-1, shiftPredicate); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Reads the names of the bones from the given bone base. |
||||
* @param boneBase |
||||
* the list of bone base structures |
||||
* @return a list of bones' names |
||||
* @throws BlenderFileException |
||||
* is thrown if problems with reading the child bones' bases occur |
||||
*/ |
||||
private List<String> readBoneNames(List<Structure> boneBase) throws BlenderFileException { |
||||
List<String> result = new ArrayList<String>(); |
||||
for (Structure boneStructure : boneBase) { |
||||
int flag = ((Number) boneStructure.getFieldValue("flag")).intValue(); |
||||
if ((flag & BoneContext.SELECTED) != 0) { |
||||
result.add(boneStructure.getFieldValue("name").toString()); |
||||
} |
||||
List<Structure> childbase = ((Structure) boneStructure.getFieldValue("childbase")).evaluateListBase(); |
||||
result.addAll(this.readBoneNames(childbase)); |
||||
} |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,196 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.Edge; |
||||
import com.jme3.scene.plugins.blender.meshes.Face; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.objects.ObjectHelper; |
||||
|
||||
/** |
||||
* This modifier allows to array modifier to the object. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class MirrorModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(MirrorModifier.class.getName()); |
||||
|
||||
private static final int FLAG_MIRROR_X = 0x08; |
||||
private static final int FLAG_MIRROR_Y = 0x10; |
||||
private static final int FLAG_MIRROR_Z = 0x20; |
||||
private static final int FLAG_MIRROR_U = 0x02; |
||||
private static final int FLAG_MIRROR_V = 0x04; |
||||
private static final int FLAG_MIRROR_VERTEX_GROUP = 0x40; |
||||
private static final int FLAG_MIRROR_MERGE = 0x80; |
||||
|
||||
private boolean[] isMirrored; |
||||
private boolean mirrorU, mirrorV; |
||||
private boolean merge; |
||||
private float tolerance; |
||||
private Pointer pMirrorObject; |
||||
private boolean mirrorVGroup; |
||||
|
||||
/** |
||||
* This constructor reads mirror data from the modifier structure. The |
||||
* stored data is a map of parameters for mirror modifier. No additional data |
||||
* is loaded. |
||||
* When the modifier is applied it is necessary to get the newly created node. |
||||
* |
||||
* @param objectStructure |
||||
* the structure of the object |
||||
* @param modifierStructure |
||||
* the structure of the modifier |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public MirrorModifier(Structure modifierStructure, BlenderContext blenderContext) { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
int flag = ((Number) modifierStructure.getFieldValue("flag")).intValue(); |
||||
|
||||
isMirrored = new boolean[] { (flag & FLAG_MIRROR_X) != 0, (flag & FLAG_MIRROR_Y) != 0, (flag & FLAG_MIRROR_Z) != 0 }; |
||||
if (blenderContext.getBlenderKey().isFixUpAxis()) { |
||||
boolean temp = isMirrored[1]; |
||||
isMirrored[1] = isMirrored[2]; |
||||
isMirrored[2] = temp; |
||||
} |
||||
mirrorU = (flag & FLAG_MIRROR_U) != 0; |
||||
mirrorV = (flag & FLAG_MIRROR_V) != 0; |
||||
mirrorVGroup = (flag & FLAG_MIRROR_VERTEX_GROUP) != 0; |
||||
merge = (flag & FLAG_MIRROR_MERGE) == 0;// in this case we use == instead of != (this is not a mistake)
|
||||
|
||||
tolerance = ((Number) modifierStructure.getFieldValue("tolerance")).floatValue(); |
||||
pMirrorObject = (Pointer) modifierStructure.getFieldValue("mirror_ob"); |
||||
|
||||
if (mirrorVGroup) { |
||||
LOGGER.warning("Mirroring vertex groups is currently not supported."); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Mirror modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} else { |
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if (temporalMesh != null) { |
||||
LOGGER.log(Level.FINE, "Applying mirror modifier to: {0}", temporalMesh); |
||||
Vector3f mirrorPlaneCenter = new Vector3f(); |
||||
if (pMirrorObject.isNotNull()) { |
||||
Structure objectStructure; |
||||
try { |
||||
objectStructure = pMirrorObject.fetchData().get(0); |
||||
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); |
||||
Node object = (Node) objectHelper.toObject(objectStructure, blenderContext); |
||||
if (object != null) { |
||||
// compute the mirror object coordinates in node's local space
|
||||
mirrorPlaneCenter = this.getWorldMatrix(node).invertLocal().mult(object.getWorldTranslation()); |
||||
} |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.SEVERE, "Cannot load mirror''s reference object. Cause: {0}", e.getLocalizedMessage()); |
||||
LOGGER.log(Level.SEVERE, "Mirror modifier will not be applied to node named: {0}", node.getName()); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
LOGGER.finest("Allocating temporal variables."); |
||||
float d; |
||||
Vector3f mirrorPlaneNormal = new Vector3f(); |
||||
Vector3f shiftVector = new Vector3f(); |
||||
|
||||
LOGGER.fine("Mirroring mesh."); |
||||
for (int mirrorIndex = 0; mirrorIndex < 3; ++mirrorIndex) { |
||||
if (isMirrored[mirrorIndex]) { |
||||
boolean mirrorAtPoint0 = mirrorPlaneCenter.get(mirrorIndex) == 0; |
||||
if (!mirrorAtPoint0) {// compute mirror's plane normal vector in node's space
|
||||
mirrorPlaneNormal.set(0, 0, 0).set(mirrorIndex, Math.signum(mirrorPlaneCenter.get(mirrorIndex))); |
||||
} |
||||
|
||||
TemporalMesh mirror = temporalMesh.clone(); |
||||
for (int i = 0; i < mirror.getVertexCount(); ++i) { |
||||
Vector3f vertex = mirror.getVertices().get(i); |
||||
Vector3f normal = mirror.getNormals().get(i); |
||||
|
||||
if (mirrorAtPoint0) { |
||||
d = Math.abs(vertex.get(mirrorIndex)); |
||||
shiftVector.set(0, 0, 0).set(mirrorIndex, -vertex.get(mirrorIndex)); |
||||
} else { |
||||
d = this.computeDistanceFromPlane(vertex, mirrorPlaneCenter, mirrorPlaneNormal); |
||||
mirrorPlaneNormal.mult(d, shiftVector); |
||||
} |
||||
|
||||
if (merge && d <= tolerance) { |
||||
vertex.addLocal(shiftVector); |
||||
normal.set(mirrorIndex, 0); |
||||
temporalMesh.getVertices().get(i).addLocal(shiftVector); |
||||
temporalMesh.getNormals().get(i).set(mirrorIndex, 0); |
||||
} else { |
||||
vertex.addLocal(shiftVector.multLocal(2)); |
||||
normal.set(mirrorIndex, -normal.get(mirrorIndex)); |
||||
} |
||||
} |
||||
|
||||
// flipping the indexes
|
||||
for (Face face : mirror.getFaces()) { |
||||
face.flipIndexes(); |
||||
} |
||||
for (Edge edge : mirror.getEdges()) { |
||||
edge.flipIndexes(); |
||||
} |
||||
Collections.reverse(mirror.getPoints()); |
||||
|
||||
if (mirrorU || mirrorV) { |
||||
for (Face face : mirror.getFaces()) { |
||||
face.flipUV(mirrorU, mirrorV); |
||||
} |
||||
} |
||||
|
||||
temporalMesh.append(mirror); |
||||
} |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fetches the world matrix transformation of the given node. |
||||
* @param node |
||||
* the node |
||||
* @return the node's world transformation matrix |
||||
*/ |
||||
private Matrix4f getWorldMatrix(Node node) { |
||||
Matrix4f result = new Matrix4f(); |
||||
result.setTranslation(node.getWorldTranslation()); |
||||
result.setRotationQuaternion(node.getWorldRotation()); |
||||
result.setScale(node.getWorldScale()); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method computes the distance between a point and a plane (described by point in space and normal vector). |
||||
* @param p |
||||
* the point in the space |
||||
* @param c |
||||
* mirror's plane center |
||||
* @param n |
||||
* mirror's plane normal (should be normalized) |
||||
* @return the minimum distance from point to plane |
||||
*/ |
||||
private float computeDistanceFromPlane(Vector3f p, Vector3f c, Vector3f n) { |
||||
return Math.abs(n.dot(p) - c.dot(n)); |
||||
} |
||||
} |
@ -0,0 +1,92 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.List; |
||||
|
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
|
||||
/** |
||||
* This class represents an object's modifier. The modifier object can be varied |
||||
* and the user needs to know what is the type of it for the specified type |
||||
* name. For example "ArmatureModifierData" type specified in blender is |
||||
* represented by AnimData object from jMonkeyEngine. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public abstract class Modifier { |
||||
public static final String ARRAY_MODIFIER_DATA = "ArrayModifierData"; |
||||
public static final String ARMATURE_MODIFIER_DATA = "ArmatureModifierData"; |
||||
public static final String PARTICLE_MODIFIER_DATA = "ParticleSystemModifierData"; |
||||
public static final String MIRROR_MODIFIER_DATA = "MirrorModifierData"; |
||||
public static final String SUBSURF_MODIFIER_DATA = "SubsurfModifierData"; |
||||
public static final String OBJECT_ANIMATION_MODIFIER_DATA = "ObjectAnimationModifierData"; |
||||
|
||||
/** This variable indicates if the modifier is invalid (<b>true</b>) or not (<b>false</b>). */ |
||||
protected boolean invalid; |
||||
/** |
||||
* A variable that tells if the modifier causes modification. Some modifiers like ArmatureModifier might have no |
||||
* Armature object attached and thus not really modifying the feature. In such cases it is good to know if it is |
||||
* sense to add the modifier to the list of object's modifiers. |
||||
*/ |
||||
protected boolean modifying = true; |
||||
|
||||
/** |
||||
* This method applies the modifier to the given node. |
||||
* |
||||
* @param node |
||||
* the node that will have modifier applied |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public abstract void apply(Node node, BlenderContext blenderContext); |
||||
|
||||
/** |
||||
* The method that is called when geometries are already created. |
||||
* @param node |
||||
* the node that will have the modifier applied |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public void postMeshCreationApply(Node node, BlenderContext blenderContext) { |
||||
} |
||||
|
||||
/** |
||||
* Determines if the modifier can be applied multiple times over one mesh. |
||||
* At this moment only armature and object animation modifiers cannot be |
||||
* applied multiple times. |
||||
* |
||||
* @param modifierType |
||||
* the type name of the modifier |
||||
* @return <b>true</b> if the modifier can be applied many times and |
||||
* <b>false</b> otherwise |
||||
*/ |
||||
public static boolean canBeAppliedMultipleTimes(String modifierType) { |
||||
return !(ARMATURE_MODIFIER_DATA.equals(modifierType) || OBJECT_ANIMATION_MODIFIER_DATA.equals(modifierType)); |
||||
} |
||||
|
||||
protected boolean validate(Structure modifierStructure, BlenderContext blenderContext) { |
||||
Structure modifierData = (Structure) modifierStructure.getFieldValue("modifier"); |
||||
Pointer pError = (Pointer) modifierData.getFieldValue("error"); |
||||
invalid = pError.isNotNull(); |
||||
return !invalid; |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the modifier causes feature's modification or <b>false</b> if not |
||||
*/ |
||||
public boolean isModifying() { |
||||
return modifying; |
||||
} |
||||
|
||||
protected TemporalMesh getTemporalMesh(Node node) { |
||||
List<Spatial> children = node.getChildren(); |
||||
if (children != null && children.size() == 1 && children.get(0) instanceof TemporalMesh) { |
||||
return (TemporalMesh) children.get(0); |
||||
} |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,117 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
/** |
||||
* A class that is used in modifiers calculations. |
||||
* |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class ModifierHelper extends AbstractBlenderHelper { |
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ModifierHelper.class.getName()); |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. |
||||
* Some functionalities may differ in different blender versions. |
||||
* |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public ModifierHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* This method reads the given object's modifiers. |
||||
* |
||||
* @param objectStructure |
||||
* the object structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public Collection<Modifier> readModifiers(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
Set<String> alreadyReadModifiers = new HashSet<String>(); |
||||
Collection<Modifier> result = new ArrayList<Modifier>(); |
||||
Structure modifiersListBase = (Structure) objectStructure.getFieldValue("modifiers"); |
||||
List<Structure> modifiers = modifiersListBase.evaluateListBase(); |
||||
for (Structure modifierStructure : modifiers) { |
||||
String modifierType = modifierStructure.getType(); |
||||
if (!Modifier.canBeAppliedMultipleTimes(modifierType) && alreadyReadModifiers.contains(modifierType)) { |
||||
LOGGER.log(Level.WARNING, "Modifier {0} can only be applied once to object: {1}", new Object[] { modifierType, objectStructure.getName() }); |
||||
} else { |
||||
Modifier modifier = null; |
||||
if (Modifier.ARRAY_MODIFIER_DATA.equals(modifierStructure.getType())) { |
||||
modifier = new ArrayModifier(modifierStructure, blenderContext); |
||||
} else if (Modifier.MIRROR_MODIFIER_DATA.equals(modifierStructure.getType())) { |
||||
modifier = new MirrorModifier(modifierStructure, blenderContext); |
||||
} else if (Modifier.ARMATURE_MODIFIER_DATA.equals(modifierStructure.getType())) { |
||||
modifier = new ArmatureModifier(objectStructure, modifierStructure, blenderContext); |
||||
} else if (Modifier.PARTICLE_MODIFIER_DATA.equals(modifierStructure.getType())) { |
||||
modifier = new ParticlesModifier(modifierStructure, blenderContext); |
||||
} else if(Modifier.SUBSURF_MODIFIER_DATA.equals(modifierStructure.getType())) { |
||||
modifier = new SubdivisionSurfaceModifier(modifierStructure, blenderContext); |
||||
} |
||||
|
||||
if (modifier != null) { |
||||
if (modifier.isModifying()) { |
||||
result.add(modifier); |
||||
alreadyReadModifiers.add(modifierType); |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "The modifier {0} will cause no changes in the model. It will be ignored!", modifierStructure.getName()); |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Unsupported modifier type: {0}", modifierStructure.getType()); |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.effect.ParticleEmitter; |
||||
import com.jme3.effect.shapes.EmitterMeshVertexShape; |
||||
import com.jme3.effect.shapes.EmitterShape; |
||||
import com.jme3.material.Material; |
||||
import com.jme3.scene.Geometry; |
||||
import com.jme3.scene.Mesh; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.particles.ParticlesHelper; |
||||
|
||||
/** |
||||
* This modifier allows to add particles to the object. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ParticlesModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(MirrorModifier.class.getName()); |
||||
|
||||
/** Loaded particles emitter. */ |
||||
private ParticleEmitter particleEmitter; |
||||
|
||||
/** |
||||
* This constructor reads the particles system structure and stores it in |
||||
* order to apply it later to the node. |
||||
* |
||||
* @param modifierStructure |
||||
* the structure of the modifier |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is throw wneh there are problems with the |
||||
* blender file |
||||
*/ |
||||
public ParticlesModifier(Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
Pointer pParticleSystem = (Pointer) modifierStructure.getFieldValue("psys"); |
||||
if (pParticleSystem.isNotNull()) { |
||||
ParticlesHelper particlesHelper = blenderContext.getHelper(ParticlesHelper.class); |
||||
Structure particleSystem = pParticleSystem.fetchData().get(0); |
||||
particleEmitter = particlesHelper.toParticleEmitter(particleSystem); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void postMeshCreationApply(Node node, BlenderContext blenderContext) { |
||||
LOGGER.log(Level.FINE, "Applying particles modifier to: {0}", node); |
||||
|
||||
MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); |
||||
ParticleEmitter emitter = particleEmitter.clone(); |
||||
|
||||
// veryfying the alpha function for particles' texture
|
||||
Integer alphaFunction = MaterialHelper.ALPHA_MASK_HYPERBOLE; |
||||
char nameSuffix = emitter.getName().charAt(emitter.getName().length() - 1); |
||||
if (nameSuffix == 'B' || nameSuffix == 'N') { |
||||
alphaFunction = MaterialHelper.ALPHA_MASK_NONE; |
||||
} |
||||
// removing the type suffix from the name
|
||||
emitter.setName(emitter.getName().substring(0, emitter.getName().length() - 1)); |
||||
|
||||
// applying emitter shape
|
||||
EmitterShape emitterShape = emitter.getShape(); |
||||
List<Mesh> meshes = new ArrayList<Mesh>(); |
||||
for (Spatial spatial : node.getChildren()) { |
||||
if (spatial instanceof Geometry) { |
||||
Mesh mesh = ((Geometry) spatial).getMesh(); |
||||
if (mesh != null) { |
||||
meshes.add(mesh); |
||||
Material material = materialHelper.getParticlesMaterial(((Geometry) spatial).getMaterial(), alphaFunction, blenderContext); |
||||
emitter.setMaterial(material);// TODO: divide into several pieces
|
||||
} |
||||
} |
||||
} |
||||
if (meshes.size() > 0 && emitterShape instanceof EmitterMeshVertexShape) { |
||||
((EmitterMeshVertexShape) emitterShape).setMeshes(meshes); |
||||
} |
||||
|
||||
node.attachChild(emitter); |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Particles modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} else { |
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if(temporalMesh != null) { |
||||
temporalMesh.applyAfterMeshCreate(this); |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,642 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.Set; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.Edge; |
||||
import com.jme3.scene.plugins.blender.meshes.Face; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.textures.TexturePixel; |
||||
|
||||
/** |
||||
* A modifier that subdivides the mesh using either simple or catmull-clark subdivision. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class SubdivisionSurfaceModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(SubdivisionSurfaceModifier.class.getName()); |
||||
|
||||
private static final int TYPE_CATMULLCLARK = 0; |
||||
private static final int TYPE_SIMPLE = 1; |
||||
|
||||
private static final int FLAG_SUBDIVIDE_UVS = 0x8; |
||||
|
||||
/** The subdivision type. */ |
||||
private int subdivType; |
||||
/** The amount of subdivision levels. */ |
||||
private int levels; |
||||
/** Indicates if the UV's should also be subdivided. */ |
||||
private boolean subdivideUVS; |
||||
/** Stores the vertices that are located on original edges of the mesh. */ |
||||
private Set<Integer> verticesOnOriginalEdges = new HashSet<Integer>(); |
||||
|
||||
/** |
||||
* Constructor loads all necessary modifier data. |
||||
* @param modifierStructure |
||||
* the modifier structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public SubdivisionSurfaceModifier(Structure modifierStructure, BlenderContext blenderContext) { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
subdivType = ((Number) modifierStructure.getFieldValue("subdivType")).intValue(); |
||||
levels = ((Number) modifierStructure.getFieldValue("levels")).intValue(); |
||||
int flag = ((Number) modifierStructure.getFieldValue("flags")).intValue(); |
||||
subdivideUVS = (flag & FLAG_SUBDIVIDE_UVS) != 0 && subdivType == TYPE_CATMULLCLARK; |
||||
|
||||
if (subdivType != TYPE_CATMULLCLARK && subdivType != TYPE_SIMPLE) { |
||||
LOGGER.log(Level.SEVERE, "Unknown subdivision type: {0}.", subdivType); |
||||
invalid = true; |
||||
} |
||||
if (levels < 0) { |
||||
LOGGER.severe("The amount of subdivision levels cannot be negative."); |
||||
invalid = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Subdivision surface modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} else if (levels > 0) {// no need to do anything if the levels is set to zero
|
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if (temporalMesh != null) { |
||||
LOGGER.log(Level.FINE, "Applying subdivision surface modifier to: {0}", temporalMesh); |
||||
verticesOnOriginalEdges.clear();//in case the instance of this class was used more than once
|
||||
|
||||
for (Edge edge : temporalMesh.getEdges()) { |
||||
verticesOnOriginalEdges.add(edge.getFirstIndex()); |
||||
verticesOnOriginalEdges.add(edge.getSecondIndex()); |
||||
} |
||||
|
||||
if (subdivType == TYPE_CATMULLCLARK) { |
||||
for (int i = 0; i < levels; ++i) { |
||||
this.subdivideSimple(temporalMesh);// first do simple subdivision ...
|
||||
this.subdivideCatmullClark(temporalMesh);// ... and then apply Catmull-Clark algorithm
|
||||
if (subdivideUVS) {// UV's can be subdivided only for Catmull-Clark subdivision algorithm
|
||||
this.subdivideUVs(temporalMesh); |
||||
} |
||||
} |
||||
} else { |
||||
for (int i = 0; i < levels; ++i) { |
||||
this.subdivideSimple(temporalMesh); |
||||
} |
||||
} |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Catmull-Clark subdivision. It assumes that the mesh was already simple-subdivided. |
||||
* @param temporalMesh |
||||
* the mesh whose vertices will be transformed to form Catmull-Clark subdivision |
||||
*/ |
||||
private void subdivideCatmullClark(TemporalMesh temporalMesh) { |
||||
Set<Integer> boundaryVertices = new HashSet<Integer>(); |
||||
for (Edge edge : temporalMesh.getEdges()) { |
||||
if (!edge.isInFace()) { |
||||
boundaryVertices.add(edge.getFirstIndex()); |
||||
boundaryVertices.add(edge.getSecondIndex()); |
||||
} else { |
||||
if (temporalMesh.isBoundary(edge.getFirstIndex())) { |
||||
boundaryVertices.add(edge.getFirstIndex()); |
||||
} |
||||
if (temporalMesh.isBoundary(edge.getSecondIndex())) { |
||||
boundaryVertices.add(edge.getSecondIndex()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
List<CreasePoint> creasePoints = new ArrayList<CreasePoint>(temporalMesh.getVertexCount()); |
||||
for (int i = 0; i < temporalMesh.getVertexCount(); ++i) { |
||||
// finding adjacent edges that were created by dividing original edges
|
||||
List<Edge> adjacentOriginalEdges = new ArrayList<Edge>(); |
||||
Collection<Edge> adjacentEdges = temporalMesh.getAdjacentEdges(i); |
||||
if(adjacentEdges != null) {// this can be null if a vertex with index 'i' is not connected to any face nor edge
|
||||
for (Edge edge : temporalMesh.getAdjacentEdges(i)) { |
||||
if (verticesOnOriginalEdges.contains(edge.getFirstIndex()) || verticesOnOriginalEdges.contains(edge.getSecondIndex())) { |
||||
adjacentOriginalEdges.add(edge); |
||||
} |
||||
} |
||||
|
||||
creasePoints.add(new CreasePoint(i, boundaryVertices.contains(i), adjacentOriginalEdges, temporalMesh)); |
||||
} else { |
||||
creasePoints.add(null);//the count of crease points must be equal to vertex count; otherwise we'll get IndexOutofBoundsException later
|
||||
} |
||||
} |
||||
|
||||
Vector3f[] averageVert = new Vector3f[temporalMesh.getVertexCount()]; |
||||
int[] averageCount = new int[temporalMesh.getVertexCount()]; |
||||
|
||||
for (Face face : temporalMesh.getFaces()) { |
||||
Vector3f centroid = face.computeCentroid(); |
||||
|
||||
for (Integer index : face.getIndexes()) { |
||||
if (boundaryVertices.contains(index)) { |
||||
Edge edge = this.findEdge(temporalMesh, index, face.getIndexes().getNextIndex(index)); |
||||
if (temporalMesh.isBoundary(edge)) { |
||||
averageVert[index] = averageVert[index] == null ? edge.computeCentroid() : averageVert[index].addLocal(edge.computeCentroid()); |
||||
averageCount[index] += 1; |
||||
} |
||||
edge = this.findEdge(temporalMesh, face.getIndexes().getPreviousIndex(index), index); |
||||
if (temporalMesh.isBoundary(edge)) { |
||||
averageVert[index] = averageVert[index] == null ? edge.computeCentroid() : averageVert[index].addLocal(edge.computeCentroid()); |
||||
averageCount[index] += 1; |
||||
} |
||||
} else { |
||||
averageVert[index] = averageVert[index] == null ? centroid.clone() : averageVert[index].addLocal(centroid); |
||||
averageCount[index] += 1; |
||||
} |
||||
} |
||||
} |
||||
for (Edge edge : temporalMesh.getEdges()) { |
||||
if (!edge.isInFace()) { |
||||
Vector3f centroid = temporalMesh.getVertices().get(edge.getFirstIndex()).add(temporalMesh.getVertices().get(edge.getSecondIndex())).divideLocal(2); |
||||
|
||||
averageVert[edge.getFirstIndex()] = averageVert[edge.getFirstIndex()] == null ? centroid.clone() : averageVert[edge.getFirstIndex()].addLocal(centroid); |
||||
averageVert[edge.getSecondIndex()] = averageVert[edge.getSecondIndex()] == null ? centroid.clone() : averageVert[edge.getSecondIndex()].addLocal(centroid); |
||||
averageCount[edge.getFirstIndex()] += 1; |
||||
averageCount[edge.getSecondIndex()] += 1; |
||||
} |
||||
} |
||||
|
||||
for (int i = 0; i < averageVert.length; ++i) { |
||||
if(averageVert[i] != null && averageCount[i] > 0) { |
||||
Vector3f v = temporalMesh.getVertices().get(i); |
||||
averageVert[i].divideLocal(averageCount[i]); |
||||
|
||||
// computing translation vector
|
||||
Vector3f t = averageVert[i].subtract(v); |
||||
if (!boundaryVertices.contains(i)) { |
||||
t.multLocal(4 / (float) averageCount[i]); |
||||
} |
||||
|
||||
// moving the vertex
|
||||
v.addLocal(t); |
||||
|
||||
// applying crease weight if necessary
|
||||
CreasePoint creasePoint = creasePoints.get(i); |
||||
if (creasePoint.getTarget() != null && creasePoint.getWeight() != 0) { |
||||
t = creasePoint.getTarget().subtractLocal(v).multLocal(creasePoint.getWeight()); |
||||
v.addLocal(t); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method performs a simple subdivision of the mesh. |
||||
* |
||||
* @param temporalMesh |
||||
* the mesh to be subdivided |
||||
*/ |
||||
private void subdivideSimple(TemporalMesh temporalMesh) { |
||||
Map<Edge, Integer> edgePoints = new HashMap<Edge, Integer>(); |
||||
Map<Face, Integer> facePoints = new HashMap<Face, Integer>(); |
||||
Set<Face> newFaces = new LinkedHashSet<Face>(); |
||||
Set<Edge> newEdges = new LinkedHashSet<Edge>(temporalMesh.getEdges().size() * 4); |
||||
|
||||
int originalFacesCount = temporalMesh.getFaces().size(); |
||||
|
||||
List<Map<String, Float>> vertexGroups = temporalMesh.getVertexGroups(); |
||||
// the result vertex array will have verts in the following order [[original_verts], [face_verts], [edge_verts]]
|
||||
List<Vector3f> vertices = temporalMesh.getVertices(); |
||||
List<Vector3f> edgeVertices = new ArrayList<Vector3f>(); |
||||
List<Vector3f> faceVertices = new ArrayList<Vector3f>(); |
||||
// the same goes for normals
|
||||
List<Vector3f> normals = temporalMesh.getNormals(); |
||||
List<Vector3f> edgeNormals = new ArrayList<Vector3f>(); |
||||
List<Vector3f> faceNormals = new ArrayList<Vector3f>(); |
||||
|
||||
List<Face> faces = temporalMesh.getFaces(); |
||||
for (Face face : faces) { |
||||
Map<String, List<Vector2f>> uvSets = face.getUvSets(); |
||||
|
||||
Vector3f facePoint = face.computeCentroid(); |
||||
Integer facePointIndex = vertices.size() + faceVertices.size(); |
||||
facePoints.put(face, facePointIndex); |
||||
faceVertices.add(facePoint); |
||||
faceNormals.add(this.computeFaceNormal(face)); |
||||
Map<String, Vector2f> faceUV = this.computeFaceUVs(face); |
||||
byte[] faceVertexColor = this.computeFaceVertexColor(face); |
||||
Map<String, Float> faceVertexGroups = this.computeFaceVertexGroups(face); |
||||
if (vertexGroups.size() > 0) { |
||||
vertexGroups.add(faceVertexGroups); |
||||
} |
||||
|
||||
for (int i = 0; i < face.getIndexes().size(); ++i) { |
||||
int vIndex = face.getIndexes().get(i); |
||||
int vPrevIndex = i == 0 ? face.getIndexes().get(face.getIndexes().size() - 1) : face.getIndexes().get(i - 1); |
||||
int vNextIndex = i == face.getIndexes().size() - 1 ? face.getIndexes().get(0) : face.getIndexes().get(i + 1); |
||||
|
||||
Edge prevEdge = this.findEdge(temporalMesh, vPrevIndex, vIndex); |
||||
Edge nextEdge = this.findEdge(temporalMesh, vIndex, vNextIndex); |
||||
int vPrevEdgeVertIndex = edgePoints.containsKey(prevEdge) ? edgePoints.get(prevEdge) : -1; |
||||
int vNextEdgeVertIndex = edgePoints.containsKey(nextEdge) ? edgePoints.get(nextEdge) : -1; |
||||
|
||||
Vector3f v = temporalMesh.getVertices().get(vIndex); |
||||
if (vPrevEdgeVertIndex < 0) { |
||||
vPrevEdgeVertIndex = vertices.size() + originalFacesCount + edgeVertices.size(); |
||||
verticesOnOriginalEdges.add(vPrevEdgeVertIndex); |
||||
edgeVertices.add(vertices.get(vPrevIndex).add(v).divideLocal(2)); |
||||
edgeNormals.add(normals.get(vPrevIndex).add(normals.get(vIndex)).normalizeLocal()); |
||||
edgePoints.put(prevEdge, vPrevEdgeVertIndex); |
||||
if (vertexGroups.size() > 0) { |
||||
vertexGroups.add(this.interpolateVertexGroups(Arrays.asList(vertexGroups.get(vPrevIndex), vertexGroups.get(vIndex)))); |
||||
} |
||||
} |
||||
if (vNextEdgeVertIndex < 0) { |
||||
vNextEdgeVertIndex = vertices.size() + originalFacesCount + edgeVertices.size(); |
||||
verticesOnOriginalEdges.add(vNextEdgeVertIndex); |
||||
edgeVertices.add(vertices.get(vNextIndex).add(v).divideLocal(2)); |
||||
edgeNormals.add(normals.get(vNextIndex).add(normals.get(vIndex)).normalizeLocal()); |
||||
edgePoints.put(nextEdge, vNextEdgeVertIndex); |
||||
if (vertexGroups.size() > 0) { |
||||
vertexGroups.add(this.interpolateVertexGroups(Arrays.asList(vertexGroups.get(vNextIndex), vertexGroups.get(vIndex)))); |
||||
} |
||||
} |
||||
|
||||
Integer[] indexes = new Integer[] { vIndex, vNextEdgeVertIndex, facePointIndex, vPrevEdgeVertIndex }; |
||||
|
||||
Map<String, List<Vector2f>> newUVSets = null; |
||||
if (uvSets != null) { |
||||
newUVSets = new HashMap<String, List<Vector2f>>(uvSets.size()); |
||||
for (Entry<String, List<Vector2f>> uvset : uvSets.entrySet()) { |
||||
int indexOfvIndex = i; |
||||
int indexOfvPrevIndex = face.getIndexes().indexOf(vPrevIndex); |
||||
int indexOfvNextIndex = face.getIndexes().indexOf(vNextIndex); |
||||
|
||||
Vector2f uv1 = uvset.getValue().get(indexOfvIndex); |
||||
Vector2f uv2 = uvset.getValue().get(indexOfvNextIndex).add(uv1).divideLocal(2); |
||||
Vector2f uv3 = faceUV.get(uvset.getKey()); |
||||
Vector2f uv4 = uvset.getValue().get(indexOfvPrevIndex).add(uv1).divideLocal(2); |
||||
List<Vector2f> uvList = Arrays.asList(uv1, uv2, uv3, uv4); |
||||
newUVSets.put(uvset.getKey(), new ArrayList<Vector2f>(uvList)); |
||||
} |
||||
} |
||||
|
||||
List<byte[]> vertexColors = null; |
||||
if (face.getVertexColors() != null) { |
||||
|
||||
int indexOfvIndex = i; |
||||
int indexOfvPrevIndex = face.getIndexes().indexOf(vPrevIndex); |
||||
int indexOfvNextIndex = face.getIndexes().indexOf(vNextIndex); |
||||
|
||||
byte[] vCol1 = face.getVertexColors().get(indexOfvIndex); |
||||
byte[] vCol2 = this.interpolateVertexColors(face.getVertexColors().get(indexOfvNextIndex), vCol1); |
||||
byte[] vCol3 = faceVertexColor; |
||||
byte[] vCol4 = this.interpolateVertexColors(face.getVertexColors().get(indexOfvPrevIndex), vCol1); |
||||
vertexColors = new ArrayList<byte[]>(Arrays.asList(vCol1, vCol2, vCol3, vCol4)); |
||||
} |
||||
|
||||
newFaces.add(new Face(indexes, face.isSmooth(), face.getMaterialNumber(), newUVSets, vertexColors, temporalMesh)); |
||||
|
||||
newEdges.add(new Edge(vIndex, vNextEdgeVertIndex, nextEdge.getCrease(), true, temporalMesh)); |
||||
newEdges.add(new Edge(vNextEdgeVertIndex, facePointIndex, 0, true, temporalMesh)); |
||||
newEdges.add(new Edge(facePointIndex, vPrevEdgeVertIndex, 0, true, temporalMesh)); |
||||
newEdges.add(new Edge(vPrevEdgeVertIndex, vIndex, prevEdge.getCrease(), true, temporalMesh)); |
||||
} |
||||
} |
||||
|
||||
vertices.addAll(faceVertices); |
||||
vertices.addAll(edgeVertices); |
||||
normals.addAll(faceNormals); |
||||
normals.addAll(edgeNormals); |
||||
|
||||
for (Edge edge : temporalMesh.getEdges()) { |
||||
if (!edge.isInFace()) { |
||||
int newVertexIndex = vertices.size(); |
||||
vertices.add(vertices.get(edge.getFirstIndex()).add(vertices.get(edge.getSecondIndex())).divideLocal(2)); |
||||
normals.add(normals.get(edge.getFirstIndex()).add(normals.get(edge.getSecondIndex())).normalizeLocal()); |
||||
|
||||
newEdges.add(new Edge(edge.getFirstIndex(), newVertexIndex, edge.getCrease(), false, temporalMesh)); |
||||
newEdges.add(new Edge(newVertexIndex, edge.getSecondIndex(), edge.getCrease(), false, temporalMesh)); |
||||
verticesOnOriginalEdges.add(newVertexIndex); |
||||
} |
||||
} |
||||
|
||||
temporalMesh.getFaces().clear(); |
||||
temporalMesh.getFaces().addAll(newFaces); |
||||
temporalMesh.getEdges().clear(); |
||||
temporalMesh.getEdges().addAll(newEdges); |
||||
|
||||
temporalMesh.rebuildIndexesMappings(); |
||||
} |
||||
|
||||
/** |
||||
* The method subdivides mesh's UV coordinates. It actually performs only Catmull-Clark modifications because if any UV's are present then they are |
||||
* automatically subdivided by the simple algorithm. |
||||
* @param temporalMesh |
||||
* the mesh whose UV coordinates will be applied Catmull-Clark algorithm |
||||
*/ |
||||
private void subdivideUVs(TemporalMesh temporalMesh) { |
||||
List<Face> faces = temporalMesh.getFaces(); |
||||
Map<String, UvCoordsSubdivideTemporalMesh> subdividedUVS = new HashMap<String, UvCoordsSubdivideTemporalMesh>(); |
||||
for (Face face : faces) { |
||||
if (face.getUvSets() != null) { |
||||
for (Entry<String, List<Vector2f>> uvset : face.getUvSets().entrySet()) { |
||||
UvCoordsSubdivideTemporalMesh uvCoordsSubdivideTemporalMesh = subdividedUVS.get(uvset.getKey()); |
||||
if (uvCoordsSubdivideTemporalMesh == null) { |
||||
try { |
||||
uvCoordsSubdivideTemporalMesh = new UvCoordsSubdivideTemporalMesh(temporalMesh.getBlenderContext()); |
||||
} catch (BlenderFileException e) { |
||||
assert false : "Something went really wrong! The UvCoordsSubdivideTemporalMesh class should NOT throw exceptions here!"; |
||||
} |
||||
subdividedUVS.put(uvset.getKey(), uvCoordsSubdivideTemporalMesh); |
||||
} |
||||
uvCoordsSubdivideTemporalMesh.addFace(uvset.getValue()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
for (Entry<String, UvCoordsSubdivideTemporalMesh> entry : subdividedUVS.entrySet()) { |
||||
entry.getValue().rebuildIndexesMappings(); |
||||
this.subdivideCatmullClark(entry.getValue()); |
||||
|
||||
for (int i = 0; i < faces.size(); ++i) { |
||||
List<Vector2f> uvs = faces.get(i).getUvSets().get(entry.getKey()); |
||||
if (uvs != null) { |
||||
uvs.clear(); |
||||
uvs.addAll(entry.getValue().faceToUVs(i)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The method computes the face's normal vector. |
||||
* @param face |
||||
* the face of the mesh |
||||
* @return face's normal vector |
||||
*/ |
||||
private Vector3f computeFaceNormal(Face face) { |
||||
Vector3f result = new Vector3f(); |
||||
for (Integer index : face.getIndexes()) { |
||||
result.addLocal(face.getTemporalMesh().getNormals().get(index)); |
||||
} |
||||
result.divideLocal(face.getIndexes().size()); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method computes the UV coordinates of the face middle point. |
||||
* @param face |
||||
* the face of the mesh |
||||
* @return a map whose key is the name of the UV set and value is the UV coordinate of the face's middle point |
||||
*/ |
||||
private Map<String, Vector2f> computeFaceUVs(Face face) { |
||||
Map<String, Vector2f> result = null; |
||||
|
||||
Map<String, List<Vector2f>> uvSets = face.getUvSets(); |
||||
if (uvSets != null && uvSets.size() > 0) { |
||||
result = new HashMap<String, Vector2f>(uvSets.size()); |
||||
|
||||
for (Entry<String, List<Vector2f>> entry : uvSets.entrySet()) { |
||||
Vector2f faceUV = new Vector2f(); |
||||
for (Vector2f uv : entry.getValue()) { |
||||
faceUV.addLocal(uv); |
||||
} |
||||
faceUV.divideLocal(entry.getValue().size()); |
||||
result.put(entry.getKey(), faceUV); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The mesh interpolates the values of vertex groups weights for new vertices. |
||||
* @param vertexGroups |
||||
* the vertex groups |
||||
* @return interpolated weights of given vertex groups' weights |
||||
*/ |
||||
private Map<String, Float> interpolateVertexGroups(List<Map<String, Float>> vertexGroups) { |
||||
Map<String, Float> weightSums = new HashMap<String, Float>(); |
||||
if (vertexGroups.size() > 0) { |
||||
for (Map<String, Float> vGroup : vertexGroups) { |
||||
for (Entry<String, Float> entry : vGroup.entrySet()) { |
||||
if (weightSums.containsKey(entry.getKey())) { |
||||
weightSums.put(entry.getKey(), weightSums.get(entry.getKey()) + entry.getValue()); |
||||
} else { |
||||
weightSums.put(entry.getKey(), entry.getValue()); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Map<String, Float> result = new HashMap<String, Float>(weightSums.size()); |
||||
for (Entry<String, Float> entry : weightSums.entrySet()) { |
||||
result.put(entry.getKey(), entry.getValue() / vertexGroups.size()); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method computes the vertex groups values for face's middle point. |
||||
* @param face |
||||
* the face of the mesh |
||||
* @return face's middle point interpolated vertex groups' weights |
||||
*/ |
||||
private Map<String, Float> computeFaceVertexGroups(Face face) { |
||||
if (face.getTemporalMesh().getVertexGroups().size() > 0) { |
||||
List<Map<String, Float>> vertexGroups = new ArrayList<Map<String, Float>>(face.getIndexes().size()); |
||||
for (Integer index : face.getIndexes()) { |
||||
vertexGroups.add(face.getTemporalMesh().getVertexGroups().get(index)); |
||||
} |
||||
return this.interpolateVertexGroups(vertexGroups); |
||||
} |
||||
return new HashMap<String, Float>(); |
||||
} |
||||
|
||||
/** |
||||
* The method computes face's middle point vertex color. |
||||
* @param face |
||||
* the face of the mesh |
||||
* @return face's middle point vertex color |
||||
*/ |
||||
private byte[] computeFaceVertexColor(Face face) { |
||||
if (face.getVertexColors() != null) { |
||||
return this.interpolateVertexColors(face.getVertexColors().toArray(new byte[face.getVertexColors().size()][])); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* The method computes the average value for the given vertex colors. |
||||
* @param colors |
||||
* the vertex colors |
||||
* @return vertex colors' average value |
||||
*/ |
||||
private byte[] interpolateVertexColors(byte[]... colors) { |
||||
TexturePixel pixel = new TexturePixel(); |
||||
TexturePixel temp = new TexturePixel(); |
||||
for (int i = 0; i < colors.length; ++i) { |
||||
temp.fromARGB8(colors[i][3], colors[i][0], colors[i][1], colors[i][2]); |
||||
pixel.add(temp); |
||||
} |
||||
pixel.divide(colors.length); |
||||
byte[] result = new byte[4]; |
||||
pixel.toRGBA8(result); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method finds an edge between the given vertices in the mesh. |
||||
* @param temporalMesh |
||||
* the mesh |
||||
* @param index1 |
||||
* first index of the edge |
||||
* @param index2 |
||||
* second index of the edge |
||||
* @return found edge or null |
||||
*/ |
||||
private Edge findEdge(TemporalMesh temporalMesh, int index1, int index2) { |
||||
for (Edge edge : temporalMesh.getEdges()) { |
||||
if (edge.getFirstIndex() == index1 && edge.getSecondIndex() == index2 || edge.getFirstIndex() == index2 && edge.getSecondIndex() == index1) { |
||||
return edge; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* This is a helper class for UV coordinates subdivision. UV's form a mesh that is being applied the same algorithms as a regular mesh. |
||||
* This way one code handles two issues. After applying Catmull-Clark algorithm the UV-mesh is transformed back into UV coordinates. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class UvCoordsSubdivideTemporalMesh extends TemporalMesh { |
||||
private static final Vector3f NORMAL = new Vector3f(0, 0, 1); |
||||
|
||||
public UvCoordsSubdivideTemporalMesh(BlenderContext blenderContext) throws BlenderFileException { |
||||
super(null, blenderContext, false); |
||||
} |
||||
|
||||
/** |
||||
* Adds a UV-face to the mesh. |
||||
* @param uvs |
||||
* the UV coordinates |
||||
*/ |
||||
public void addFace(List<Vector2f> uvs) { |
||||
Integer[] indexes = new Integer[uvs.size()]; |
||||
int i = 0; |
||||
|
||||
for (Vector2f uv : uvs) { |
||||
Vector3f v = new Vector3f(uv.x, uv.y, 0); |
||||
int index = vertices.indexOf(v); |
||||
if (index >= 0) { |
||||
indexes[i++] = index; |
||||
} else { |
||||
indexes[i++] = vertices.size(); |
||||
vertices.add(v); |
||||
normals.add(NORMAL); |
||||
} |
||||
} |
||||
faces.add(new Face(indexes, false, 0, null, null, this)); |
||||
for (i = 1; i < indexes.length; ++i) { |
||||
edges.add(new Edge(indexes[i - 1], indexes[i], 0, true, this)); |
||||
} |
||||
edges.add(new Edge(indexes[indexes.length - 1], indexes[0], 0, true, this)); |
||||
} |
||||
|
||||
/** |
||||
* Converts the mesh back into UV coordinates for the given face. |
||||
* @param faceIndex |
||||
* the index of the face |
||||
* @return UV coordinates |
||||
*/ |
||||
public List<Vector2f> faceToUVs(int faceIndex) { |
||||
Face face = faces.get(faceIndex); |
||||
List<Vector2f> result = new ArrayList<Vector2f>(face.getIndexes().size()); |
||||
for (Integer index : face.getIndexes()) { |
||||
Vector3f v = vertices.get(index); |
||||
result.add(new Vector2f(v.x, v.y)); |
||||
} |
||||
return result; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A point computed for each vertex before applying CC subdivision and after simple subdivision. |
||||
* This class has a target where the vertices will be drawn to with a proper strength (value from 0 to 1). |
||||
* |
||||
* The algorithm of computing the target point was made by observing how blender behaves. |
||||
* If a vertex has one or less creased edges (means edges that have non zero crease value) the target will not exist. |
||||
* If a vertex is a border vertex and has two creased edges - the target will be the original simple subdivided vertex. |
||||
* If a vertex is not a border vertex and have two creased edges - then it will be drawned to the plane defined by those |
||||
* two edges. |
||||
* If a vertex has 3 or more creased edges it will be drawn to its original vertex before CC subdivision with average strength |
||||
* computed from edges' crease values. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class CreasePoint { |
||||
private Vector3f target = new Vector3f(); |
||||
private float weight; |
||||
private int index; |
||||
|
||||
public CreasePoint(int index, boolean borderIndex, List<Edge> creaseEdges, TemporalMesh temporalMesh) { |
||||
this.index = index; |
||||
if (creaseEdges == null || creaseEdges.size() <= 1) { |
||||
target = null;// crease is used when vertex belongs to at least 2 creased edges
|
||||
} else { |
||||
int creasedEdgesCount = 0; |
||||
for (Edge edge : creaseEdges) { |
||||
if (edge.getCrease() > 0) { |
||||
++creasedEdgesCount; |
||||
weight += edge.getCrease(); |
||||
target.addLocal(temporalMesh.getVertices().get(edge.getOtherIndex(index))); |
||||
} |
||||
} |
||||
|
||||
if (creasedEdgesCount <= 1) { |
||||
target = null;// crease is used when vertex belongs to at least 2 creased edges
|
||||
} else if (creasedEdgesCount == 2) { |
||||
if (borderIndex) { |
||||
target.set(temporalMesh.getVertices().get(index)); |
||||
} else { |
||||
target.addLocal(temporalMesh.getVertices().get(index)).divideLocal(creasedEdgesCount + 1); |
||||
} |
||||
} else { |
||||
target.set(temporalMesh.getVertices().get(index)); |
||||
} |
||||
if (creasedEdgesCount > 0) { |
||||
weight /= creasedEdgesCount; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public Vector3f getTarget() { |
||||
return target; |
||||
} |
||||
|
||||
public float getWeight() { |
||||
return weight; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "CreasePoint [index = " + index + ", target=" + target + ", weight=" + weight + "]"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,54 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
|
||||
/** |
||||
* The triangulation modifier. It does not take any settings into account so if the result is different than |
||||
* in blender then please apply the modifier before importing. |
||||
* |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class TriangulateModifier extends Modifier { |
||||
private static final Logger LOGGER = Logger.getLogger(TriangulateModifier.class.getName()); |
||||
|
||||
/** |
||||
* This constructor reads animation data from the object structore. The |
||||
* stored data is the AnimData and additional data is armature's OMA. |
||||
* |
||||
* @param objectStructure |
||||
* the structure of the object |
||||
* @param modifierStructure |
||||
* the structure of the modifier |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blender file is somehow |
||||
* corrupted |
||||
*/ |
||||
public TriangulateModifier(Structure objectStructure, Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
LOGGER.warning("Triangulation modifier does not take modifier options into account. If triangulation result is different" + " than the model in blender please apply the modifier before importing!"); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void apply(Node node, BlenderContext blenderContext) { |
||||
if (invalid) { |
||||
LOGGER.log(Level.WARNING, "Triangulate modifier is invalid! Cannot be applied to: {0}", node.getName()); |
||||
} |
||||
TemporalMesh temporalMesh = this.getTemporalMesh(node); |
||||
if (temporalMesh != null) { |
||||
LOGGER.log(Level.FINE, "Applying triangulation modifier to: {0}", temporalMesh); |
||||
temporalMesh.triangulate(); |
||||
} else { |
||||
LOGGER.log(Level.WARNING, "Cannot find temporal mesh for node: {0}. The modifier will NOT be applied!", node); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,520 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.objects; |
||||
|
||||
import java.nio.Buffer; |
||||
import java.nio.FloatBuffer; |
||||
import java.nio.IntBuffer; |
||||
import java.nio.ShortBuffer; |
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.light.Light; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Matrix4f; |
||||
import com.jme3.math.Transform; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.renderer.Camera; |
||||
import com.jme3.scene.CameraNode; |
||||
import com.jme3.scene.Geometry; |
||||
import com.jme3.scene.LightNode; |
||||
import com.jme3.scene.Mesh.Mode; |
||||
import com.jme3.scene.Node; |
||||
import com.jme3.scene.Spatial; |
||||
import com.jme3.scene.Spatial.CullHint; |
||||
import com.jme3.scene.VertexBuffer.Type; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.animations.AnimationHelper; |
||||
import com.jme3.scene.plugins.blender.cameras.CameraHelper; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.curves.CurvesHelper; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.lights.LightHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.MeshHelper; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.modifiers.Modifier; |
||||
import com.jme3.scene.plugins.blender.modifiers.ModifierHelper; |
||||
import com.jme3.util.TempVars; |
||||
|
||||
/** |
||||
* A class that is used in object calculations. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class ObjectHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(ObjectHelper.class.getName()); |
||||
|
||||
public static final String OMA_MARKER = "oma"; |
||||
public static final String ARMATURE_NODE_MARKER = "armature-node"; |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. |
||||
* Some functionalities may differ in different blender versions. |
||||
* |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public ObjectHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* This method reads the given structure and createn an object that |
||||
* represents the data. |
||||
* |
||||
* @param objectStructure |
||||
* the object's structure |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return blener's object representation or null if its type is excluded from loading |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when the given data is inapropriate |
||||
*/ |
||||
public Object toObject(Structure objectStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
Object loadedResult = blenderContext.getLoadedFeature(objectStructure.getOldMemoryAddress(), LoadedDataType.FEATURE); |
||||
if (loadedResult != null) { |
||||
return loadedResult; |
||||
} |
||||
|
||||
LOGGER.fine("Loading blender object."); |
||||
if ("ID".equals(objectStructure.getType())) { |
||||
Node object = (Node) this.loadLibrary(objectStructure); |
||||
if (object.getParent() != null) { |
||||
LOGGER.log(Level.FINEST, "Detaching object {0}, loaded from external file, from its parent.", object); |
||||
object.getParent().detachChild(object); |
||||
} |
||||
return object; |
||||
} |
||||
int type = ((Number) objectStructure.getFieldValue("type")).intValue(); |
||||
ObjectType objectType = ObjectType.valueOf(type); |
||||
LOGGER.log(Level.FINE, "Type of the object: {0}.", objectType); |
||||
|
||||
int lay = ((Number) objectStructure.getFieldValue("lay")).intValue(); |
||||
if ((lay & blenderContext.getBlenderKey().getLayersToLoad()) == 0) { |
||||
LOGGER.fine("The layer this object is located in is not included in loading."); |
||||
return null; |
||||
} |
||||
|
||||
blenderContext.pushParent(objectStructure); |
||||
String name = objectStructure.getName(); |
||||
LOGGER.log(Level.FINE, "Loading obejct: {0}", name); |
||||
|
||||
int restrictflag = ((Number) objectStructure.getFieldValue("restrictflag")).intValue(); |
||||
boolean visible = (restrictflag & 0x01) != 0; |
||||
|
||||
Pointer pParent = (Pointer) objectStructure.getFieldValue("parent"); |
||||
Object parent = blenderContext.getLoadedFeature(pParent.getOldMemoryAddress(), LoadedDataType.FEATURE); |
||||
if (parent == null && pParent.isNotNull()) { |
||||
Structure parentStructure = pParent.fetchData().get(0); |
||||
parent = this.toObject(parentStructure, blenderContext); |
||||
} |
||||
|
||||
Transform t = this.getTransformation(objectStructure, blenderContext); |
||||
LOGGER.log(Level.FINE, "Importing object of type: {0}", objectType); |
||||
Node result = null; |
||||
try { |
||||
switch (objectType) { |
||||
case LATTICE: |
||||
case METABALL: |
||||
case TEXT: |
||||
case WAVE: |
||||
LOGGER.log(Level.WARNING, "{0} type is not supported but the node will be returned in order to keep parent - child relationship.", objectType); |
||||
case EMPTY: |
||||
case ARMATURE: |
||||
// need to use an empty node to properly create
|
||||
// parent-children relationships between nodes
|
||||
result = new Node(name); |
||||
break; |
||||
case MESH: |
||||
result = new Node(name); |
||||
MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); |
||||
Pointer pMesh = (Pointer) objectStructure.getFieldValue("data"); |
||||
List<Structure> meshesArray = pMesh.fetchData(); |
||||
TemporalMesh temporalMesh = meshHelper.toTemporalMesh(meshesArray.get(0), blenderContext); |
||||
if (temporalMesh != null) { |
||||
result.attachChild(temporalMesh); |
||||
} |
||||
break; |
||||
case SURF: |
||||
case CURVE: |
||||
result = new Node(name); |
||||
Pointer pCurve = (Pointer) objectStructure.getFieldValue("data"); |
||||
if (pCurve.isNotNull()) { |
||||
CurvesHelper curvesHelper = blenderContext.getHelper(CurvesHelper.class); |
||||
Structure curveData = pCurve.fetchData().get(0); |
||||
TemporalMesh curvesTemporalMesh = curvesHelper.toCurve(curveData, blenderContext); |
||||
if (curvesTemporalMesh != null) { |
||||
result.attachChild(curvesTemporalMesh); |
||||
} |
||||
} |
||||
break; |
||||
case LAMP: |
||||
Pointer pLamp = (Pointer) objectStructure.getFieldValue("data"); |
||||
if (pLamp.isNotNull()) { |
||||
LightHelper lightHelper = blenderContext.getHelper(LightHelper.class); |
||||
List<Structure> lampsArray = pLamp.fetchData(); |
||||
Light light = lightHelper.toLight(lampsArray.get(0), blenderContext); |
||||
if (light == null) { |
||||
// probably some light type is not supported, just create a node so that we can maintain child-parent relationship for nodes
|
||||
result = new Node(name); |
||||
} else { |
||||
result = new LightNode(name, light); |
||||
} |
||||
} |
||||
break; |
||||
case CAMERA: |
||||
Pointer pCamera = (Pointer) objectStructure.getFieldValue("data"); |
||||
if (pCamera.isNotNull()) { |
||||
CameraHelper cameraHelper = blenderContext.getHelper(CameraHelper.class); |
||||
List<Structure> camerasArray = pCamera.fetchData(); |
||||
Camera camera = cameraHelper.toCamera(camerasArray.get(0), blenderContext); |
||||
if (camera == null) { |
||||
// just create a node so that we can maintain child-parent relationship for nodes
|
||||
result = new Node(name); |
||||
} else { |
||||
result = new CameraNode(name, camera); |
||||
} |
||||
} |
||||
break; |
||||
default: |
||||
LOGGER.log(Level.WARNING, "Unsupported object type: {0}", type); |
||||
} |
||||
|
||||
if (result != null) { |
||||
LOGGER.fine("Storing loaded feature in blender context and applying markers (those will be removed before the final result is released)."); |
||||
Long oma = objectStructure.getOldMemoryAddress(); |
||||
blenderContext.addLoadedFeatures(oma, LoadedDataType.STRUCTURE, objectStructure); |
||||
blenderContext.addLoadedFeatures(oma, LoadedDataType.FEATURE, result); |
||||
|
||||
blenderContext.addMarker(OMA_MARKER, result, objectStructure.getOldMemoryAddress()); |
||||
if (objectType == ObjectType.ARMATURE) { |
||||
blenderContext.addMarker(ARMATURE_NODE_MARKER, result, Boolean.TRUE); |
||||
} |
||||
|
||||
result.setLocalTransform(t); |
||||
result.setCullHint(visible ? CullHint.Always : CullHint.Inherit); |
||||
if (parent instanceof Node) { |
||||
((Node) parent).attachChild(result); |
||||
} |
||||
|
||||
LOGGER.fine("Reading and applying object's modifiers."); |
||||
ModifierHelper modifierHelper = blenderContext.getHelper(ModifierHelper.class); |
||||
Collection<Modifier> modifiers = modifierHelper.readModifiers(objectStructure, blenderContext); |
||||
for (Modifier modifier : modifiers) { |
||||
modifier.apply(result, blenderContext); |
||||
} |
||||
|
||||
if (result.getChildren() != null && result.getChildren().size() > 0) { |
||||
if (result.getChildren().size() == 1 && result.getChild(0) instanceof TemporalMesh) { |
||||
LOGGER.fine("Converting temporal mesh into jme geometries."); |
||||
((TemporalMesh) result.getChild(0)).toGeometries(); |
||||
} |
||||
|
||||
LOGGER.fine("Applying proper scale to the geometries."); |
||||
for (Spatial child : result.getChildren()) { |
||||
if (child instanceof Geometry) { |
||||
this.flipMeshIfRequired((Geometry) child, child.getWorldScale()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// I prefer do compute bounding box here than read it from the file
|
||||
result.updateModelBound(); |
||||
|
||||
LOGGER.fine("Applying animations to the object if such are defined."); |
||||
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); |
||||
animationHelper.applyAnimations(result, blenderContext.getBlenderKey().getAnimationMatchMethod()); |
||||
|
||||
LOGGER.fine("Loading constraints connected with this object."); |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
constraintHelper.loadConstraints(objectStructure, blenderContext); |
||||
|
||||
LOGGER.fine("Loading custom properties."); |
||||
if (blenderContext.getBlenderKey().isLoadObjectProperties()) { |
||||
Properties properties = this.loadProperties(objectStructure, blenderContext); |
||||
// the loaded property is a group property, so we need to get
|
||||
// each value and set it to Spatial
|
||||
if (properties != null && properties.getValue() != null) { |
||||
this.applyProperties(result, properties); |
||||
} |
||||
} |
||||
} |
||||
} finally { |
||||
blenderContext.popParent(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method flips the mesh if the scale is mirroring it. Mirroring scale has either 1 or all 3 factors negative. |
||||
* If two factors are negative then there is no mirroring because a rotation and translation can be found that will |
||||
* lead to the same transform when all scales are positive. |
||||
* |
||||
* @param geometry |
||||
* the geometry that is being flipped if necessary |
||||
* @param scale |
||||
* the scale vector of the given geometry |
||||
*/ |
||||
private void flipMeshIfRequired(Geometry geometry, Vector3f scale) { |
||||
float s = scale.x * scale.y * scale.z; |
||||
|
||||
if (s < 0 && geometry.getMesh() != null) {// negative s means that the scale is mirroring the object
|
||||
FloatBuffer normals = geometry.getMesh().getFloatBuffer(Type.Normal); |
||||
if (normals != null) { |
||||
for (int i = 0; i < normals.limit(); i += 3) { |
||||
if (scale.x < 0) { |
||||
normals.put(i, -normals.get(i)); |
||||
} |
||||
if (scale.y < 0) { |
||||
normals.put(i + 1, -normals.get(i + 1)); |
||||
} |
||||
if (scale.z < 0) { |
||||
normals.put(i + 2, -normals.get(i + 2)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (geometry.getMesh().getMode() == Mode.Triangles) {// there is no need to flip the indexes for lines and points
|
||||
LOGGER.finer("Flipping index order in triangle mesh."); |
||||
Buffer indexBuffer = geometry.getMesh().getBuffer(Type.Index).getData(); |
||||
for (int i = 0; i < indexBuffer.limit(); i += 3) { |
||||
if (indexBuffer instanceof ShortBuffer) { |
||||
short index = ((ShortBuffer) indexBuffer).get(i + 1); |
||||
((ShortBuffer) indexBuffer).put(i + 1, ((ShortBuffer) indexBuffer).get(i + 2)); |
||||
((ShortBuffer) indexBuffer).put(i + 2, index); |
||||
} else { |
||||
int index = ((IntBuffer) indexBuffer).get(i + 1); |
||||
((IntBuffer) indexBuffer).put(i + 1, ((IntBuffer) indexBuffer).get(i + 2)); |
||||
((IntBuffer) indexBuffer).put(i + 2, index); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Checks if the first given OMA points to a parent of the second one. |
||||
* The parent need not to be the direct one. This method should be called when we are sure |
||||
* that both of the features are alred loaded because it does not check it. |
||||
* The OMA's should point to a spatials, otherwise the function will throw ClassCastException. |
||||
* @param supposedParentOMA |
||||
* the OMA of the node that we suppose might be a parent of the second one |
||||
* @param spatialOMA |
||||
* the OMA of the scene's node |
||||
* @return <b>true</b> if the first given OMA points to a parent of the second one and <b>false</b> otherwise |
||||
*/ |
||||
public boolean isParent(Long supposedParentOMA, Long spatialOMA) { |
||||
Spatial supposedParent = (Spatial) blenderContext.getLoadedFeature(supposedParentOMA, LoadedDataType.FEATURE); |
||||
Spatial spatial = (Spatial) blenderContext.getLoadedFeature(spatialOMA, LoadedDataType.FEATURE); |
||||
|
||||
Spatial parent = spatial.getParent(); |
||||
while (parent != null) { |
||||
if (parent.equals(supposedParent)) { |
||||
return true; |
||||
} |
||||
parent = parent.getParent(); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* This method calculates local transformation for the object. Parentage is |
||||
* taken under consideration. |
||||
* |
||||
* @param objectStructure |
||||
* the object's structure |
||||
* @return objects transformation relative to its parent |
||||
*/ |
||||
public Transform getTransformation(Structure objectStructure, BlenderContext blenderContext) { |
||||
TempVars tempVars = TempVars.get(); |
||||
|
||||
Matrix4f parentInv = tempVars.tempMat4; |
||||
Pointer pParent = (Pointer) objectStructure.getFieldValue("parent"); |
||||
if (pParent.isNotNull()) { |
||||
Structure parentObjectStructure = (Structure) blenderContext.getLoadedFeature(pParent.getOldMemoryAddress(), LoadedDataType.STRUCTURE); |
||||
this.getMatrix(parentObjectStructure, "obmat", fixUpAxis, parentInv).invertLocal(); |
||||
} else { |
||||
parentInv.loadIdentity(); |
||||
} |
||||
|
||||
Matrix4f globalMatrix = this.getMatrix(objectStructure, "obmat", fixUpAxis, tempVars.tempMat42); |
||||
Matrix4f localMatrix = parentInv.multLocal(globalMatrix); |
||||
|
||||
this.getSizeSignums(objectStructure, tempVars.vect1); |
||||
|
||||
localMatrix.toTranslationVector(tempVars.vect2); |
||||
localMatrix.toRotationQuat(tempVars.quat1); |
||||
localMatrix.toScaleVector(tempVars.vect3); |
||||
|
||||
Transform t = new Transform(tempVars.vect2, tempVars.quat1.normalizeLocal(), tempVars.vect3.multLocal(tempVars.vect1)); |
||||
tempVars.release(); |
||||
return t; |
||||
} |
||||
|
||||
/** |
||||
* The method gets the signs of the scale factors and stores them properly in the given vector. |
||||
* @param objectStructure |
||||
* the object's structure |
||||
* @param store |
||||
* the vector where the result will be stored |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private void getSizeSignums(Structure objectStructure, Vector3f store) { |
||||
DynamicArray<Number> size = (DynamicArray<Number>) objectStructure.getFieldValue("size"); |
||||
if (fixUpAxis) { |
||||
store.x = Math.signum(size.get(0).floatValue()); |
||||
store.y = Math.signum(size.get(2).floatValue()); |
||||
store.z = Math.signum(size.get(1).floatValue()); |
||||
} else { |
||||
store.x = Math.signum(size.get(0).floatValue()); |
||||
store.y = Math.signum(size.get(1).floatValue()); |
||||
store.z = Math.signum(size.get(2).floatValue()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the matrix of a given name for the given structure. |
||||
* It takes up axis into consideration. |
||||
* |
||||
* The method that moves the matrix from Z-up axis to Y-up axis space is as follows: |
||||
* - load the matrix directly from blender (it has the Z-up axis orientation) |
||||
* - switch the second and third rows in the matrix |
||||
* - switch the second and third column in the matrix |
||||
* - multiply the values in the third row by -1 |
||||
* - multiply the values in the third column by -1 |
||||
* |
||||
* The result matrix is now in Y-up axis orientation. |
||||
* The procedure was discovered by experimenting but it looks like it's working :) |
||||
* The previous procedure transformet the loaded matrix into component (loc, rot, scale), |
||||
* switched several values and pu the back into the matrix. |
||||
* It worked fine until models with negative scale are used. |
||||
* The current method is not touched by that flaw. |
||||
* |
||||
* @param structure |
||||
* the structure with matrix data |
||||
* @param matrixName |
||||
* the name of the matrix |
||||
* @param fixUpAxis |
||||
* tells if the Y axis is a UP axis |
||||
* @param store |
||||
* the matrix where the result will pe placed |
||||
* @return the required matrix |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private Matrix4f getMatrix(Structure structure, String matrixName, boolean fixUpAxis, Matrix4f store) { |
||||
DynamicArray<Number> obmat = (DynamicArray<Number>) structure.getFieldValue(matrixName); |
||||
// the matrix must be square
|
||||
int rowAndColumnSize = Math.abs((int) Math.sqrt(obmat.getTotalSize())); |
||||
for (int i = 0; i < rowAndColumnSize; ++i) { |
||||
for (int j = 0; j < rowAndColumnSize; ++j) { |
||||
float value = obmat.get(j, i).floatValue(); |
||||
if (Math.abs(value) <= FastMath.FLT_EPSILON) { |
||||
value = 0; |
||||
} |
||||
store.set(i, j, value); |
||||
} |
||||
} |
||||
if (fixUpAxis) { |
||||
// first switch the second and third row
|
||||
for (int i = 0; i < 4; ++i) { |
||||
float temp = store.get(1, i); |
||||
store.set(1, i, store.get(2, i)); |
||||
store.set(2, i, temp); |
||||
} |
||||
|
||||
// then switch the second and third column
|
||||
for (int i = 0; i < 4; ++i) { |
||||
float temp = store.get(i, 1); |
||||
store.set(i, 1, store.get(i, 2)); |
||||
store.set(i, 2, temp); |
||||
} |
||||
|
||||
// multiply the values in the third row by -1
|
||||
store.m20 *= -1; |
||||
store.m21 *= -1; |
||||
store.m22 *= -1; |
||||
store.m23 *= -1; |
||||
|
||||
// multiply the values in the third column by -1
|
||||
store.m02 *= -1; |
||||
store.m12 *= -1; |
||||
store.m22 *= -1; |
||||
store.m32 *= -1; |
||||
} |
||||
|
||||
return store; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the matrix of a given name for the given structure. |
||||
* It takes up axis into consideration. |
||||
* |
||||
* @param structure |
||||
* the structure with matrix data |
||||
* @param matrixName |
||||
* the name of the matrix |
||||
* @param fixUpAxis |
||||
* tells if the Y axis is a UP axis |
||||
* @return the required matrix |
||||
*/ |
||||
public Matrix4f getMatrix(Structure structure, String matrixName, boolean fixUpAxis) { |
||||
return this.getMatrix(structure, matrixName, fixUpAxis, new Matrix4f()); |
||||
} |
||||
|
||||
private static enum ObjectType { |
||||
EMPTY(0), MESH(1), CURVE(2), SURF(3), TEXT(4), METABALL(5), LAMP(10), CAMERA(11), WAVE(21), LATTICE(22), ARMATURE(25); |
||||
|
||||
private int blenderTypeValue; |
||||
|
||||
private ObjectType(int blenderTypeValue) { |
||||
this.blenderTypeValue = blenderTypeValue; |
||||
} |
||||
|
||||
public static ObjectType valueOf(int blenderTypeValue) throws BlenderFileException { |
||||
for (ObjectType type : ObjectType.values()) { |
||||
if (type.blenderTypeValue == blenderTypeValue) { |
||||
return type; |
||||
} |
||||
} |
||||
throw new BlenderFileException("Unknown type value: " + blenderTypeValue); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,365 @@ |
||||
package com.jme3.scene.plugins.blender.objects; |
||||
|
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.BlenderInputStream; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* The blender object's custom properties. |
||||
* This class is valid for all versions of blender. |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class Properties implements Cloneable { |
||||
// property type
|
||||
public static final int IDP_STRING = 0; |
||||
public static final int IDP_INT = 1; |
||||
public static final int IDP_FLOAT = 2; |
||||
public static final int IDP_ARRAY = 5; |
||||
public static final int IDP_GROUP = 6; |
||||
// public static final int IDP_ID = 7;//this is not implemented in blender (yet)
|
||||
public static final int IDP_DOUBLE = 8; |
||||
// the following are valid for blender 2.5x+
|
||||
public static final int IDP_IDPARRAY = 9; |
||||
public static final int IDP_NUMTYPES = 10; |
||||
|
||||
protected static final String RNA_PROPERTY_NAME = "_RNA_UI"; |
||||
/** Default name of the property (used if the name is not specified in blender file). */ |
||||
protected static final String DEFAULT_NAME = "Unnamed property"; |
||||
|
||||
/** The name of the property. */ |
||||
private String name; |
||||
/** The type of the property. */ |
||||
private int type; |
||||
/** The subtype of the property. Defines the type of array's elements. */ |
||||
private int subType; |
||||
/** The value of the property. */ |
||||
private Object value; |
||||
/** The description of the property. */ |
||||
private String description; |
||||
|
||||
/** |
||||
* This method loads the property from the belnder file. |
||||
* @param idPropertyStructure |
||||
* the ID structure constining the property |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when the belnder file is somehow invalid |
||||
*/ |
||||
public void load(Structure idPropertyStructure, BlenderContext blenderContext) throws BlenderFileException { |
||||
name = idPropertyStructure.getFieldValue("name").toString(); |
||||
if (name == null || name.length() == 0) { |
||||
name = DEFAULT_NAME; |
||||
} |
||||
subType = ((Number) idPropertyStructure.getFieldValue("subtype")).intValue(); |
||||
type = ((Number) idPropertyStructure.getFieldValue("type")).intValue(); |
||||
|
||||
// reading the data
|
||||
Structure data = (Structure) idPropertyStructure.getFieldValue("data"); |
||||
int len = ((Number) idPropertyStructure.getFieldValue("len")).intValue(); |
||||
switch (type) { |
||||
case IDP_STRING: { |
||||
Pointer pointer = (Pointer) data.getFieldValue("pointer"); |
||||
BlenderInputStream bis = blenderContext.getInputStream(); |
||||
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pointer.getOldMemoryAddress()); |
||||
bis.setPosition(dataFileBlock.getBlockPosition()); |
||||
value = bis.readString(); |
||||
break; |
||||
} |
||||
case IDP_INT: |
||||
int intValue = ((Number) data.getFieldValue("val")).intValue(); |
||||
value = Integer.valueOf(intValue); |
||||
break; |
||||
case IDP_FLOAT: |
||||
int floatValue = ((Number) data.getFieldValue("val")).intValue(); |
||||
value = Float.valueOf(Float.intBitsToFloat(floatValue)); |
||||
break; |
||||
case IDP_ARRAY: { |
||||
Pointer pointer = (Pointer) data.getFieldValue("pointer"); |
||||
BlenderInputStream bis = blenderContext.getInputStream(); |
||||
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pointer.getOldMemoryAddress()); |
||||
bis.setPosition(dataFileBlock.getBlockPosition()); |
||||
int elementAmount = dataFileBlock.getSize(); |
||||
switch (subType) { |
||||
case IDP_INT: |
||||
elementAmount /= 4; |
||||
int[] intList = new int[elementAmount]; |
||||
for (int i = 0; i < elementAmount; ++i) { |
||||
intList[i] = bis.readInt(); |
||||
} |
||||
value = intList; |
||||
break; |
||||
case IDP_FLOAT: |
||||
elementAmount /= 4; |
||||
float[] floatList = new float[elementAmount]; |
||||
for (int i = 0; i < elementAmount; ++i) { |
||||
floatList[i] = bis.readFloat(); |
||||
} |
||||
value = floatList; |
||||
break; |
||||
case IDP_DOUBLE: |
||||
elementAmount /= 8; |
||||
double[] doubleList = new double[elementAmount]; |
||||
for (int i = 0; i < elementAmount; ++i) { |
||||
doubleList[i] = bis.readDouble(); |
||||
} |
||||
value = doubleList; |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Invalid array subtype: " + subType); |
||||
} |
||||
} |
||||
case IDP_GROUP: |
||||
Structure group = (Structure) data.getFieldValue("group"); |
||||
List<Structure> dataList = group.evaluateListBase(); |
||||
List<Properties> subProperties = new ArrayList<Properties>(len); |
||||
for (Structure d : dataList) { |
||||
Properties properties = new Properties(); |
||||
properties.load(d, blenderContext); |
||||
subProperties.add(properties); |
||||
} |
||||
value = subProperties; |
||||
break; |
||||
case IDP_DOUBLE: |
||||
int doublePart1 = ((Number) data.getFieldValue("val")).intValue(); |
||||
int doublePart2 = ((Number) data.getFieldValue("val2")).intValue(); |
||||
long doubleVal = (long) doublePart2 << 32 | doublePart1; |
||||
value = Double.valueOf(Double.longBitsToDouble(doubleVal)); |
||||
break; |
||||
case IDP_IDPARRAY: { |
||||
Pointer pointer = (Pointer) data.getFieldValue("pointer"); |
||||
List<Structure> arrays = pointer.fetchData(); |
||||
List<Object> result = new ArrayList<Object>(arrays.size()); |
||||
Properties temp = new Properties(); |
||||
for (Structure array : arrays) { |
||||
temp.load(array, blenderContext); |
||||
result.add(temp.value); |
||||
} |
||||
value = result; |
||||
break; |
||||
} |
||||
case IDP_NUMTYPES: |
||||
throw new UnsupportedOperationException(); |
||||
// case IDP_ID://not yet implemented in blender
|
||||
// return null;
|
||||
default: |
||||
throw new IllegalStateException("Unknown custom property type: " + type); |
||||
} |
||||
this.completeLoading(); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the name of the property. |
||||
* @return the name of the property |
||||
*/ |
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the value of the property. |
||||
* The type of the value depends on the type of the property. |
||||
* @return the value of the property |
||||
*/ |
||||
public Object getValue() { |
||||
return value; |
||||
} |
||||
|
||||
/** |
||||
* @return the names of properties that are stored withing this property |
||||
* (assuming this property is of IDP_GROUP type) |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public List<String> getSubPropertiesNames() { |
||||
List<String> result = null; |
||||
if (type == IDP_GROUP) { |
||||
List<Properties> properties = (List<Properties>) value; |
||||
if (properties != null && properties.size() > 0) { |
||||
result = new ArrayList<String>(properties.size()); |
||||
for (Properties property : properties) { |
||||
result.add(property.getName()); |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the same as getValue if the current property is of |
||||
* other type than IDP_GROUP and its name matches 'propertyName' param. If |
||||
* this property is a group property the method tries to find subproperty |
||||
* value of the given name. The first found value is returnes os <b>use this |
||||
* method wisely</b>. If no property of a given name is foung - <b>null</b> |
||||
* is returned. |
||||
* |
||||
* @param propertyName |
||||
* the name of the property |
||||
* @return found property value or <b>null</b> |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public Object findValue(String propertyName) { |
||||
if (name.equals(propertyName)) { |
||||
return value; |
||||
} else { |
||||
if (type == IDP_GROUP) { |
||||
List<Properties> props = (List<Properties>) value; |
||||
for (Properties p : props) { |
||||
Object v = p.findValue(propertyName); |
||||
if (v != null) { |
||||
return v; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder sb = new StringBuilder(); |
||||
this.append(sb, new StringBuilder()); |
||||
return sb.toString(); |
||||
} |
||||
|
||||
/** |
||||
* This method appends the data of the property to the given string buffer. |
||||
* @param sb |
||||
* string buffer |
||||
* @param indent |
||||
* indent buffer |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
private void append(StringBuilder sb, StringBuilder indent) { |
||||
sb.append(indent).append("name: ").append(name).append("\n\r"); |
||||
sb.append(indent).append("type: ").append(type).append("\n\r"); |
||||
sb.append(indent).append("subType: ").append(subType).append("\n\r"); |
||||
sb.append(indent).append("description: ").append(description).append("\n\r"); |
||||
indent.append('\t'); |
||||
sb.append(indent).append("value: "); |
||||
if (value instanceof Properties) { |
||||
((Properties) value).append(sb, indent); |
||||
} else if (value instanceof List) { |
||||
for (Object v : (List<Object>) value) { |
||||
if (v instanceof Properties) { |
||||
sb.append(indent).append("{\n\r"); |
||||
indent.append('\t'); |
||||
((Properties) v).append(sb, indent); |
||||
indent.deleteCharAt(indent.length() - 1); |
||||
sb.append(indent).append("}\n\r"); |
||||
} else { |
||||
sb.append(v); |
||||
} |
||||
} |
||||
} else { |
||||
sb.append(value); |
||||
} |
||||
sb.append("\n\r"); |
||||
indent.deleteCharAt(indent.length() - 1); |
||||
} |
||||
|
||||
/** |
||||
* This method should be called after the properties loading. |
||||
* It loads the properties from the _RNA_UI property and removes this property from the |
||||
* result list. |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
protected void completeLoading() { |
||||
if (type == IDP_GROUP) { |
||||
List<Properties> groupProperties = (List<Properties>) value; |
||||
Properties rnaUI = null; |
||||
for (Properties properties : groupProperties) { |
||||
if (properties.name.equals(RNA_PROPERTY_NAME) && properties.type == IDP_GROUP) { |
||||
rnaUI = properties; |
||||
break; |
||||
} |
||||
} |
||||
if (rnaUI != null) { |
||||
// removing the RNA from the result list
|
||||
groupProperties.remove(rnaUI); |
||||
|
||||
// loading the descriptions
|
||||
Map<String, String> descriptions = new HashMap<String, String>(groupProperties.size()); |
||||
List<Properties> propertiesRNA = (List<Properties>) rnaUI.value; |
||||
for (Properties properties : propertiesRNA) { |
||||
String name = properties.name; |
||||
String description = null; |
||||
List<Properties> rnaData = (List<Properties>) properties.value; |
||||
for (Properties rna : rnaData) { |
||||
if ("description".equalsIgnoreCase(rna.name)) { |
||||
description = (String) rna.value; |
||||
break; |
||||
} |
||||
} |
||||
descriptions.put(name, description); |
||||
} |
||||
|
||||
// applying the descriptions
|
||||
for (Properties properties : groupProperties) { |
||||
properties.description = descriptions.get(properties.name); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
final int prime = 31; |
||||
int result = 1; |
||||
result = prime * result + (description == null ? 0 : description.hashCode()); |
||||
result = prime * result + (name == null ? 0 : name.hashCode()); |
||||
result = prime * result + subType; |
||||
result = prime * result + type; |
||||
result = prime * result + (value == null ? 0 : value.hashCode()); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (obj == null) { |
||||
return false; |
||||
} |
||||
if (this.getClass() != obj.getClass()) { |
||||
return false; |
||||
} |
||||
Properties other = (Properties) obj; |
||||
if (description == null) { |
||||
if (other.description != null) { |
||||
return false; |
||||
} |
||||
} else if (!description.equals(other.description)) { |
||||
return false; |
||||
} |
||||
if (name == null) { |
||||
if (other.name != null) { |
||||
return false; |
||||
} |
||||
} else if (!name.equals(other.name)) { |
||||
return false; |
||||
} |
||||
if (subType != other.subType) { |
||||
return false; |
||||
} |
||||
if (type != other.type) { |
||||
return false; |
||||
} |
||||
if (value == null) { |
||||
if (other.value != null) { |
||||
return false; |
||||
} |
||||
} else if (!value.equals(other.value)) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,192 @@ |
||||
package com.jme3.scene.plugins.blender.particles; |
||||
|
||||
import com.jme3.effect.ParticleEmitter; |
||||
import com.jme3.effect.ParticleMesh.Type; |
||||
import com.jme3.effect.influencers.EmptyParticleInfluencer; |
||||
import com.jme3.effect.influencers.NewtonianParticleInfluencer; |
||||
import com.jme3.effect.influencers.ParticleInfluencer; |
||||
import com.jme3.effect.shapes.EmitterMeshConvexHullShape; |
||||
import com.jme3.effect.shapes.EmitterMeshFaceShape; |
||||
import com.jme3.effect.shapes.EmitterMeshVertexShape; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
import java.util.logging.Logger; |
||||
|
||||
public class ParticlesHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(ParticlesHelper.class.getName()); |
||||
|
||||
// part->type
|
||||
public static final int PART_EMITTER = 0; |
||||
public static final int PART_REACTOR = 1; |
||||
public static final int PART_HAIR = 2; |
||||
public static final int PART_FLUID = 3; |
||||
|
||||
// part->flag
|
||||
public static final int PART_REACT_STA_END = 1; |
||||
public static final int PART_REACT_MULTIPLE = 2; |
||||
public static final int PART_LOOP = 4; |
||||
// public static final int PART_LOOP_INSTANT =8;
|
||||
public static final int PART_HAIR_GEOMETRY = 16; |
||||
public static final int PART_UNBORN = 32; // show unborn particles
|
||||
public static final int PART_DIED = 64; // show died particles
|
||||
public static final int PART_TRAND = 128; |
||||
public static final int PART_EDISTR = 256; // particle/face from face areas
|
||||
public static final int PART_STICKY = 512; // collided particles can stick to collider
|
||||
public static final int PART_DIE_ON_COL = 1 << 12; |
||||
public static final int PART_SIZE_DEFL = 1 << 13; // swept sphere deflections
|
||||
public static final int PART_ROT_DYN = 1 << 14; // dynamic rotation
|
||||
public static final int PART_SIZEMASS = 1 << 16; |
||||
public static final int PART_ABS_LENGTH = 1 << 15; |
||||
public static final int PART_ABS_TIME = 1 << 17; |
||||
public static final int PART_GLOB_TIME = 1 << 18; |
||||
public static final int PART_BOIDS_2D = 1 << 19; |
||||
public static final int PART_BRANCHING = 1 << 20; |
||||
public static final int PART_ANIM_BRANCHING = 1 << 21; |
||||
public static final int PART_SELF_EFFECT = 1 << 22; |
||||
public static final int PART_SYMM_BRANCHING = 1 << 24; |
||||
public static final int PART_HAIR_BSPLINE = 1024; |
||||
public static final int PART_GRID_INVERT = 1 << 26; |
||||
public static final int PART_CHILD_EFFECT = 1 << 27; |
||||
public static final int PART_CHILD_SEAMS = 1 << 28; |
||||
public static final int PART_CHILD_RENDER = 1 << 29; |
||||
public static final int PART_CHILD_GUIDE = 1 << 30; |
||||
|
||||
// part->from
|
||||
public static final int PART_FROM_VERT = 0; |
||||
public static final int PART_FROM_FACE = 1; |
||||
public static final int PART_FROM_VOLUME = 2; |
||||
public static final int PART_FROM_PARTICLE = 3; |
||||
public static final int PART_FROM_CHILD = 4; |
||||
|
||||
// part->phystype
|
||||
public static final int PART_PHYS_NO = 0; |
||||
public static final int PART_PHYS_NEWTON = 1; |
||||
public static final int PART_PHYS_KEYED = 2; |
||||
public static final int PART_PHYS_BOIDS = 3; |
||||
|
||||
// part->draw_as
|
||||
public static final int PART_DRAW_NOT = 0; |
||||
public static final int PART_DRAW_DOT = 1; |
||||
public static final int PART_DRAW_CIRC = 2; |
||||
public static final int PART_DRAW_CROSS = 3; |
||||
public static final int PART_DRAW_AXIS = 4; |
||||
public static final int PART_DRAW_LINE = 5; |
||||
public static final int PART_DRAW_PATH = 6; |
||||
public static final int PART_DRAW_OB = 7; |
||||
public static final int PART_DRAW_GR = 8; |
||||
public static final int PART_DRAW_BB = 9; |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. Some functionalities may differ in |
||||
* different blender versions. |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public ParticlesHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
public ParticleEmitter toParticleEmitter(Structure particleSystem) throws BlenderFileException { |
||||
ParticleEmitter result = null; |
||||
Pointer pParticleSettings = (Pointer) particleSystem.getFieldValue("part"); |
||||
if (pParticleSettings.isNotNull()) { |
||||
Structure particleSettings = pParticleSettings.fetchData().get(0); |
||||
|
||||
int totPart = ((Number) particleSettings.getFieldValue("totpart")).intValue(); |
||||
|
||||
// draw type will be stored temporarily in the name (it is used during modifier applying operation)
|
||||
int drawAs = ((Number) particleSettings.getFieldValue("draw_as")).intValue(); |
||||
char nameSuffix;// P - point, L - line, N - None, B - Bilboard
|
||||
switch (drawAs) { |
||||
case PART_DRAW_NOT: |
||||
nameSuffix = 'N'; |
||||
totPart = 0;// no need to generate particles in this case
|
||||
break; |
||||
case PART_DRAW_BB: |
||||
nameSuffix = 'B'; |
||||
break; |
||||
case PART_DRAW_OB: |
||||
case PART_DRAW_GR: |
||||
nameSuffix = 'P'; |
||||
LOGGER.warning("Neither object nor group particles supported yet! Using point representation instead!");// TODO: support groups and aobjects
|
||||
break; |
||||
case PART_DRAW_LINE: |
||||
nameSuffix = 'L'; |
||||
LOGGER.warning("Lines not yet supported! Using point representation instead!");// TODO: support lines
|
||||
default:// all others are rendered as points in blender
|
||||
nameSuffix = 'P'; |
||||
} |
||||
result = new ParticleEmitter(particleSettings.getName() + nameSuffix, Type.Triangle, totPart); |
||||
if (nameSuffix == 'N') { |
||||
return result;// no need to set anything else
|
||||
} |
||||
|
||||
// setting the emitters shape (the shapes meshes will be set later during modifier applying operation)
|
||||
int from = ((Number) particleSettings.getFieldValue("from")).intValue(); |
||||
switch (from) { |
||||
case PART_FROM_VERT: |
||||
result.setShape(new EmitterMeshVertexShape()); |
||||
break; |
||||
case PART_FROM_FACE: |
||||
result.setShape(new EmitterMeshFaceShape()); |
||||
break; |
||||
case PART_FROM_VOLUME: |
||||
result.setShape(new EmitterMeshConvexHullShape()); |
||||
break; |
||||
default: |
||||
LOGGER.warning("Default shape used! Unknown emitter shape value ('from' parameter: " + from + ')'); |
||||
} |
||||
|
||||
// reading acceleration
|
||||
DynamicArray<Number> acc = (DynamicArray<Number>) particleSettings.getFieldValue("acc"); |
||||
result.setGravity(-acc.get(0).floatValue(), -acc.get(1).floatValue(), -acc.get(2).floatValue()); |
||||
|
||||
// setting the colors
|
||||
result.setEndColor(new ColorRGBA(1f, 1f, 1f, 1f)); |
||||
result.setStartColor(new ColorRGBA(1f, 1f, 1f, 1f)); |
||||
|
||||
// reading size
|
||||
float sizeFactor = nameSuffix == 'B' ? 1.0f : 0.3f; |
||||
float size = ((Number) particleSettings.getFieldValue("size")).floatValue() * sizeFactor; |
||||
result.setStartSize(size); |
||||
result.setEndSize(size); |
||||
|
||||
// reading lifetime
|
||||
int fps = blenderContext.getBlenderKey().getFps(); |
||||
float lifetime = ((Number) particleSettings.getFieldValue("lifetime")).floatValue() / fps; |
||||
float randlife = ((Number) particleSettings.getFieldValue("randlife")).floatValue() / fps; |
||||
result.setLowLife(lifetime * (1.0f - randlife)); |
||||
result.setHighLife(lifetime); |
||||
|
||||
// preparing influencer
|
||||
ParticleInfluencer influencer; |
||||
int phystype = ((Number) particleSettings.getFieldValue("phystype")).intValue(); |
||||
switch (phystype) { |
||||
case PART_PHYS_NEWTON: |
||||
influencer = new NewtonianParticleInfluencer(); |
||||
((NewtonianParticleInfluencer) influencer).setNormalVelocity(((Number) particleSettings.getFieldValue("normfac")).floatValue()); |
||||
((NewtonianParticleInfluencer) influencer).setVelocityVariation(((Number) particleSettings.getFieldValue("randfac")).floatValue()); |
||||
((NewtonianParticleInfluencer) influencer).setSurfaceTangentFactor(((Number) particleSettings.getFieldValue("tanfac")).floatValue()); |
||||
((NewtonianParticleInfluencer) influencer).setSurfaceTangentRotation(((Number) particleSettings.getFieldValue("tanphase")).floatValue()); |
||||
break; |
||||
case PART_PHYS_BOIDS: |
||||
case PART_PHYS_KEYED:// TODO: support other influencers
|
||||
LOGGER.warning("Boids and Keyed particles physic not yet supported! Empty influencer used!"); |
||||
case PART_PHYS_NO: |
||||
default: |
||||
influencer = new EmptyParticleInfluencer(); |
||||
} |
||||
result.setParticleInfluencer(influencer); |
||||
} |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,401 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.TreeMap; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
/** |
||||
* A class constaining the colorband data. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class ColorBand { |
||||
private static final Logger LOGGER = Logger.getLogger(ColorBand.class.getName()); |
||||
|
||||
// interpolation types
|
||||
public static final int IPO_LINEAR = 0; |
||||
public static final int IPO_EASE = 1; |
||||
public static final int IPO_BSPLINE = 2; |
||||
public static final int IPO_CARDINAL = 3; |
||||
public static final int IPO_CONSTANT = 4; |
||||
|
||||
private int cursorsAmount, ipoType; |
||||
/** The default amount of possible cursor positions. */ |
||||
private int resultSize = 1001; |
||||
private ColorBandData[] data; |
||||
|
||||
/** |
||||
* A constructor used to instantiate color band by hand instead of reading it from the blend file. |
||||
* @param ipoType |
||||
* the interpolation type |
||||
* @param colors |
||||
* the colorband colors |
||||
* @param positions |
||||
* the positions for colors' cursors |
||||
* @param resultSize |
||||
* the size of the result table |
||||
*/ |
||||
public ColorBand(int ipoType, List<ColorRGBA> colors, List<Integer> positions, int resultSize) { |
||||
if (colors == null || colors.size() < 1) { |
||||
throw new IllegalArgumentException("The amount of colorband's colors must be at least 1."); |
||||
} |
||||
if (ipoType < IPO_LINEAR || ipoType > IPO_CONSTANT) { |
||||
throw new IllegalArgumentException("Unknown colorband interpolation type: " + ipoType); |
||||
} |
||||
if (positions == null || positions.size() != colors.size()) { |
||||
throw new IllegalArgumentException("The size of positions and colors list should be equal!"); |
||||
} |
||||
for (Integer position : positions) { |
||||
if (position.intValue() < 0 || position.intValue() >= resultSize) { |
||||
throw new IllegalArgumentException("Invalid position value: " + position + "! Should be from range: [0, " + resultSize + "]!"); |
||||
} |
||||
} |
||||
|
||||
cursorsAmount = colors.size(); |
||||
this.ipoType = ipoType; |
||||
this.resultSize = resultSize; |
||||
data = new ColorBandData[cursorsAmount]; |
||||
for (int i = 0; i < cursorsAmount; ++i) { |
||||
data[i] = new ColorBandData(colors.get(i), positions.get(i)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Loads the data from the given structure. |
||||
* @param tex |
||||
* @param blenderContext |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public ColorBand(Structure tex, BlenderContext blenderContext) { |
||||
int flag = ((Number) tex.getFieldValue("flag")).intValue(); |
||||
if ((flag & GeneratedTexture.TEX_COLORBAND) != 0) { |
||||
Pointer pColorband = (Pointer) tex.getFieldValue("coba"); |
||||
try { |
||||
Structure colorbandStructure = pColorband.fetchData().get(0); |
||||
cursorsAmount = ((Number) colorbandStructure.getFieldValue("tot")).intValue(); |
||||
ipoType = ((Number) colorbandStructure.getFieldValue("ipotype")).intValue(); |
||||
data = new ColorBandData[cursorsAmount]; |
||||
DynamicArray<Structure> data = (DynamicArray<Structure>) colorbandStructure.getFieldValue("data"); |
||||
for (int i = 0; i < cursorsAmount; ++i) { |
||||
this.data[i] = new ColorBandData(data.get(i)); |
||||
} |
||||
} catch (BlenderFileException e) { |
||||
LOGGER.log(Level.WARNING, "Cannot fetch the colorband structure. The reason: {0}", e.getLocalizedMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method determines if the colorband has any transparencies or is not |
||||
* transparent at all. |
||||
* |
||||
* @return <b>true</b> if the colorband has transparencies and <b>false</b> |
||||
* otherwise |
||||
*/ |
||||
public boolean hasTransparencies() { |
||||
if (data != null) { |
||||
for (ColorBandData colorBandData : data) { |
||||
if (colorBandData.a < 1.0f) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* This method computes the values of the colorband. |
||||
* |
||||
* @return an array of 1001 elements and each element is float[4] object |
||||
* containing rgba values |
||||
*/ |
||||
public float[][] computeValues() { |
||||
float[][] result = null; |
||||
if (data != null) { |
||||
result = new float[resultSize][4];// resultSize - amount of possible cursor positions; 4 = [r, g, b, a]
|
||||
if (data.length == 1) {// special case; use only one color for all types of colorband interpolation
|
||||
for (int i = 0; i < result.length; ++i) { |
||||
result[i][0] = data[0].r; |
||||
result[i][1] = data[0].g; |
||||
result[i][2] = data[0].b; |
||||
result[i][3] = data[0].a; |
||||
} |
||||
} else { |
||||
int currentCursor = 0; |
||||
ColorBandData currentData = data[0]; |
||||
ColorBandData nextData = data[0]; |
||||
switch (ipoType) { |
||||
case ColorBand.IPO_LINEAR: |
||||
float rDiff = 0, |
||||
gDiff = 0, |
||||
bDiff = 0, |
||||
aDiff = 0, |
||||
posDiff; |
||||
for (int i = 0; i < result.length; ++i) { |
||||
posDiff = i - currentData.pos; |
||||
result[i][0] = currentData.r + rDiff * posDiff; |
||||
result[i][1] = currentData.g + gDiff * posDiff; |
||||
result[i][2] = currentData.b + bDiff * posDiff; |
||||
result[i][3] = currentData.a + aDiff * posDiff; |
||||
if (nextData.pos == i) { |
||||
currentData = data[currentCursor++]; |
||||
if (currentCursor < data.length) { |
||||
nextData = data[currentCursor]; |
||||
// calculate differences
|
||||
int d = nextData.pos - currentData.pos; |
||||
rDiff = (nextData.r - currentData.r) / d; |
||||
gDiff = (nextData.g - currentData.g) / d; |
||||
bDiff = (nextData.b - currentData.b) / d; |
||||
aDiff = (nextData.a - currentData.a) / d; |
||||
} else { |
||||
rDiff = gDiff = bDiff = aDiff = 0; |
||||
} |
||||
} |
||||
} |
||||
break; |
||||
case ColorBand.IPO_BSPLINE: |
||||
case ColorBand.IPO_CARDINAL: |
||||
Map<Integer, ColorBandData> cbDataMap = new TreeMap<Integer, ColorBandData>(); |
||||
for (int i = 0; i < data.length; ++i) { |
||||
cbDataMap.put(Integer.valueOf(i), data[i]); |
||||
} |
||||
|
||||
if (data[0].pos == 0) { |
||||
cbDataMap.put(Integer.valueOf(-1), data[0]); |
||||
} else { |
||||
ColorBandData cbData = new ColorBandData(data[0]); |
||||
cbData.pos = 0; |
||||
cbDataMap.put(Integer.valueOf(-1), cbData); |
||||
cbDataMap.put(Integer.valueOf(-2), cbData); |
||||
} |
||||
|
||||
if (data[data.length - 1].pos == 1000) { |
||||
cbDataMap.put(Integer.valueOf(data.length), data[data.length - 1]); |
||||
} else { |
||||
ColorBandData cbData = new ColorBandData(data[data.length - 1]); |
||||
cbData.pos = 1000; |
||||
cbDataMap.put(Integer.valueOf(data.length), cbData); |
||||
cbDataMap.put(Integer.valueOf(data.length + 1), cbData); |
||||
} |
||||
|
||||
float[] ipoFactors = new float[4]; |
||||
float f; |
||||
|
||||
ColorBandData data0 = this.getColorbandData(currentCursor - 2, cbDataMap); |
||||
ColorBandData data1 = this.getColorbandData(currentCursor - 1, cbDataMap); |
||||
ColorBandData data2 = this.getColorbandData(currentCursor, cbDataMap); |
||||
ColorBandData data3 = this.getColorbandData(currentCursor + 1, cbDataMap); |
||||
|
||||
for (int i = 0; i < result.length; ++i) { |
||||
if (data2.pos != data1.pos) { |
||||
f = (i - data2.pos) / (float) (data1.pos - data2.pos); |
||||
f = FastMath.clamp(f, 0.0f, 1.0f); |
||||
} else { |
||||
f = 0.0f; |
||||
} |
||||
this.getIpoData(f, ipoFactors); |
||||
result[i][0] = ipoFactors[3] * data0.r + ipoFactors[2] * data1.r + ipoFactors[1] * data2.r + ipoFactors[0] * data3.r; |
||||
result[i][1] = ipoFactors[3] * data0.g + ipoFactors[2] * data1.g + ipoFactors[1] * data2.g + ipoFactors[0] * data3.g; |
||||
result[i][2] = ipoFactors[3] * data0.b + ipoFactors[2] * data1.b + ipoFactors[1] * data2.b + ipoFactors[0] * data3.b; |
||||
result[i][3] = ipoFactors[3] * data0.a + ipoFactors[2] * data1.a + ipoFactors[1] * data2.a + ipoFactors[0] * data3.a; |
||||
result[i][0] = FastMath.clamp(result[i][0], 0.0f, 1.0f); |
||||
result[i][1] = FastMath.clamp(result[i][1], 0.0f, 1.0f); |
||||
result[i][2] = FastMath.clamp(result[i][2], 0.0f, 1.0f); |
||||
result[i][3] = FastMath.clamp(result[i][3], 0.0f, 1.0f); |
||||
|
||||
if (nextData.pos == i) { |
||||
++currentCursor; |
||||
data0 = cbDataMap.get(currentCursor - 2); |
||||
data1 = cbDataMap.get(currentCursor - 1); |
||||
data2 = cbDataMap.get(currentCursor); |
||||
data3 = cbDataMap.get(currentCursor + 1); |
||||
} |
||||
} |
||||
break; |
||||
case ColorBand.IPO_EASE: |
||||
float d, |
||||
a, |
||||
b, |
||||
d2; |
||||
for (int i = 0; i < result.length; ++i) { |
||||
if (nextData.pos != currentData.pos) { |
||||
d = (i - currentData.pos) / (float) (nextData.pos - currentData.pos); |
||||
d2 = d * d; |
||||
a = 3.0f * d2 - 2.0f * d * d2; |
||||
b = 1.0f - a; |
||||
} else { |
||||
d = a = 0.0f; |
||||
b = 1.0f; |
||||
} |
||||
|
||||
result[i][0] = b * currentData.r + a * nextData.r; |
||||
result[i][1] = b * currentData.g + a * nextData.g; |
||||
result[i][2] = b * currentData.b + a * nextData.b; |
||||
result[i][3] = b * currentData.a + a * nextData.a; |
||||
if (nextData.pos == i) { |
||||
currentData = data[currentCursor++]; |
||||
if (currentCursor < data.length) { |
||||
nextData = data[currentCursor]; |
||||
} |
||||
} |
||||
} |
||||
break; |
||||
case ColorBand.IPO_CONSTANT: |
||||
for (int i = 0; i < result.length; ++i) { |
||||
result[i][0] = currentData.r; |
||||
result[i][1] = currentData.g; |
||||
result[i][2] = currentData.b; |
||||
result[i][3] = currentData.a; |
||||
if (nextData.pos == i) { |
||||
currentData = data[currentCursor++]; |
||||
if (currentCursor < data.length) { |
||||
nextData = data[currentCursor]; |
||||
} |
||||
} |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown interpolation type: " + ipoType); |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private ColorBandData getColorbandData(int index, Map<Integer, ColorBandData> cbDataMap) { |
||||
ColorBandData result = cbDataMap.get(index); |
||||
if (result == null) { |
||||
result = new ColorBandData(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the data for either B-spline of Cardinal |
||||
* interpolation. |
||||
* |
||||
* @param d |
||||
* distance factor for the current intensity |
||||
* @param ipoFactors |
||||
* table to store the results (size of the table must be at least |
||||
* 4) |
||||
*/ |
||||
private void getIpoData(float d, float[] ipoFactors) { |
||||
float d2 = d * d; |
||||
float d3 = d2 * d; |
||||
if (ipoType == ColorBand.IPO_BSPLINE) { |
||||
ipoFactors[0] = -0.71f * d3 + 1.42f * d2 - 0.71f * d; |
||||
ipoFactors[1] = 1.29f * d3 - 2.29f * d2 + 1.0f; |
||||
ipoFactors[2] = -1.29f * d3 + 1.58f * d2 + 0.71f * d; |
||||
ipoFactors[3] = 0.71f * d3 - 0.71f * d2; |
||||
} else if (ipoType == ColorBand.IPO_CARDINAL) { |
||||
ipoFactors[0] = -0.16666666f * d3 + 0.5f * d2 - 0.5f * d + 0.16666666f; |
||||
ipoFactors[1] = 0.5f * d3 - d2 + 0.6666666f; |
||||
ipoFactors[2] = -0.5f * d3 + 0.5f * d2 + 0.5f * d + 0.16666666f; |
||||
ipoFactors[3] = 0.16666666f * d3; |
||||
} else { |
||||
throw new IllegalStateException("Cannot get interpolation data for other colorband types than B-spline and Cardinal!"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Class to store the single colorband cursor data. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class ColorBandData { |
||||
public final float r, g, b, a; |
||||
public int pos; |
||||
|
||||
public ColorBandData() { |
||||
r = g = b = 0; |
||||
a = 1; |
||||
} |
||||
|
||||
/** |
||||
* Constructor that stores the color and position of the cursor. |
||||
* @param color |
||||
* the cursor's color |
||||
* @param pos |
||||
* the cursor's position |
||||
*/ |
||||
public ColorBandData(ColorRGBA color, int pos) { |
||||
r = color.r; |
||||
g = color.g; |
||||
b = color.b; |
||||
a = color.a; |
||||
this.pos = pos; |
||||
} |
||||
|
||||
/** |
||||
* Copy constructor. |
||||
*/ |
||||
private ColorBandData(ColorBandData data) { |
||||
r = data.r; |
||||
g = data.g; |
||||
b = data.b; |
||||
a = data.a; |
||||
pos = data.pos; |
||||
} |
||||
|
||||
/** |
||||
* Constructor. Loads the data from the given structure. |
||||
* |
||||
* @param cbdataStructure |
||||
* the structure containing the CBData object |
||||
*/ |
||||
public ColorBandData(Structure cbdataStructure) { |
||||
r = ((Number) cbdataStructure.getFieldValue("r")).floatValue(); |
||||
g = ((Number) cbdataStructure.getFieldValue("g")).floatValue(); |
||||
b = ((Number) cbdataStructure.getFieldValue("b")).floatValue(); |
||||
a = ((Number) cbdataStructure.getFieldValue("a")).floatValue(); |
||||
pos = (int) (((Number) cbdataStructure.getFieldValue("pos")).floatValue() * 1000.0f); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "P: " + pos + " [" + r + ", " + g + ", " + b + ", " + a + "]"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,543 @@ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import java.awt.Graphics2D; |
||||
import java.awt.RenderingHints; |
||||
import java.awt.image.BufferedImage; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import jme3tools.converters.ImageToAwt; |
||||
|
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.scene.Geometry; |
||||
import com.jme3.scene.Mesh; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialContext; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.textures.TriangulatedTexture.TriangleTextureElement; |
||||
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType; |
||||
import com.jme3.scene.plugins.blender.textures.UVProjectionGenerator.UVProjectionType; |
||||
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender; |
||||
import com.jme3.scene.plugins.blender.textures.blending.TextureBlenderFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.texture.Texture.MagFilter; |
||||
import com.jme3.texture.Texture.MinFilter; |
||||
import com.jme3.texture.Texture.WrapMode; |
||||
import com.jme3.texture.Texture2D; |
||||
import com.jme3.texture.TextureCubeMap; |
||||
import com.jme3.texture.image.ColorSpace; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
/** |
||||
* This class represents a texture that is defined for the material. It can be |
||||
* made of several textures (both 2D and 3D) that are merged together and |
||||
* returned as a single texture. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class CombinedTexture { |
||||
private static final Logger LOGGER = Logger.getLogger(CombinedTexture.class.getName()); |
||||
|
||||
/** The mapping type of the texture. Defined bu MaterialContext.MTEX_COL, MTEX_NOR etc. */ |
||||
private final int mappingType; |
||||
/** |
||||
* If set to true then if a texture without alpha is added then all textures below are discarded because |
||||
* the new one will cover them anyway. If set to false then all textures are stored. |
||||
*/ |
||||
private boolean discardCoveredTextures; |
||||
/** The data for each of the textures. */ |
||||
private List<TextureData> textureDatas = new ArrayList<TextureData>(); |
||||
/** The result texture. */ |
||||
private Texture resultTexture; |
||||
/** The UV values for the result texture. */ |
||||
private List<Vector2f> resultUVS; |
||||
|
||||
/** |
||||
* Constructor. Stores the texture mapping type (ie. color map, normal map). |
||||
* |
||||
* @param mappingType |
||||
* texture mapping type |
||||
* @param discardCoveredTextures |
||||
* if set to true then if a texture without alpha is added then all textures below are discarded because |
||||
* the new one will cover them anyway, if set to false then all textures are stored |
||||
*/ |
||||
public CombinedTexture(int mappingType, boolean discardCoveredTextures) { |
||||
this.mappingType = mappingType; |
||||
this.discardCoveredTextures = discardCoveredTextures; |
||||
} |
||||
|
||||
/** |
||||
* This method adds a texture data to the resulting texture. |
||||
* |
||||
* @param texture |
||||
* the source texture |
||||
* @param textureBlender |
||||
* the texture blender (to mix the texture with its material |
||||
* color) |
||||
* @param uvCoordinatesType |
||||
* the type of UV coordinates |
||||
* @param projectionType |
||||
* the type of UV coordinates projection (for flat textures) |
||||
* @param textureStructure |
||||
* the texture sructure |
||||
* @param uvCoordinatesName |
||||
* the name of the used user's UV coordinates for this texture |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public void add(Texture texture, TextureBlender textureBlender, int uvCoordinatesType, int projectionType, Structure textureStructure, String uvCoordinatesName, BlenderContext blenderContext) { |
||||
if (!(texture instanceof GeneratedTexture) && !(texture instanceof Texture2D)) { |
||||
throw new IllegalArgumentException("Unsupported texture type: " + (texture == null ? "null" : texture.getClass())); |
||||
} |
||||
if (!(texture instanceof GeneratedTexture) || blenderContext.getBlenderKey().isLoadGeneratedTextures()) { |
||||
if (UVCoordinatesGenerator.isTextureCoordinateTypeSupported(UVCoordinatesType.valueOf(uvCoordinatesType))) { |
||||
TextureData textureData = new TextureData(); |
||||
textureData.texture = texture; |
||||
textureData.textureBlender = textureBlender; |
||||
textureData.uvCoordinatesType = UVCoordinatesType.valueOf(uvCoordinatesType); |
||||
textureData.projectionType = UVProjectionType.valueOf(projectionType); |
||||
textureData.textureStructure = textureStructure; |
||||
textureData.uvCoordinatesName = uvCoordinatesName; |
||||
|
||||
if (discardCoveredTextures && textureDatas.size() > 0 && this.isWithoutAlpha(textureData, blenderContext)) { |
||||
textureDatas.clear();// clear previous textures, they will be covered anyway
|
||||
} |
||||
textureDatas.add(textureData); |
||||
} else { |
||||
LOGGER.warning("The texture coordinates type is not supported: " + UVCoordinatesType.valueOf(uvCoordinatesType) + ". The texture '" + textureStructure.getName() + "'."); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method flattens the texture and creates a single result of Texture2D |
||||
* type. |
||||
* |
||||
* @param geometry |
||||
* the geometry the texture is created for |
||||
* @param geometriesOMA |
||||
* the old memory address of the geometries list that the given |
||||
* geometry belongs to (needed for bounding box creation) |
||||
* @param userDefinedUVCoordinates |
||||
* the UV's defined by user (null or zero length table if none |
||||
* were defined) |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the name of the user UV coordinates used (null if the UV's were |
||||
* generated) |
||||
*/ |
||||
public String flatten(Geometry geometry, Long geometriesOMA, Map<String, List<Vector2f>> userDefinedUVCoordinates, BlenderContext blenderContext) { |
||||
Mesh mesh = geometry.getMesh(); |
||||
Texture previousTexture = null; |
||||
UVCoordinatesType masterUVCoordinatesType = null; |
||||
String masterUserUVSetName = null; |
||||
for (TextureData textureData : textureDatas) { |
||||
// decompress compressed textures (all will be merged into one texture anyway)
|
||||
if (textureDatas.size() > 1 && textureData.texture.getImage().getFormat().isCompressed()) { |
||||
textureData.texture.setImage(ImageUtils.decompress(textureData.texture.getImage())); |
||||
textureData.textureBlender = TextureBlenderFactory.alterTextureType(textureData.texture.getImage().getFormat(), textureData.textureBlender); |
||||
} |
||||
|
||||
if (previousTexture == null) {// the first texture will lead the others to its shape
|
||||
if (textureData.texture instanceof GeneratedTexture) { |
||||
resultTexture = ((GeneratedTexture) textureData.texture).triangulate(mesh, geometriesOMA, textureData.uvCoordinatesType, blenderContext); |
||||
} else if (textureData.texture instanceof Texture2D) { |
||||
resultTexture = textureData.texture; |
||||
|
||||
if (textureData.uvCoordinatesType == UVCoordinatesType.TEXCO_UV && userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) { |
||||
if (textureData.uvCoordinatesName == null) { |
||||
resultUVS = userDefinedUVCoordinates.values().iterator().next();// get the first UV available
|
||||
} else { |
||||
resultUVS = userDefinedUVCoordinates.get(textureData.uvCoordinatesName); |
||||
} |
||||
if(resultUVS == null && LOGGER.isLoggable(Level.WARNING)) { |
||||
LOGGER.warning("The texture " + textureData.texture.getName() + " has assigned non existing UV coordinates group: " + textureData.uvCoordinatesName + "."); |
||||
} |
||||
masterUserUVSetName = textureData.uvCoordinatesName; |
||||
} else { |
||||
TemporalMesh temporalMesh = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH); |
||||
resultUVS = UVCoordinatesGenerator.generateUVCoordinatesFor2DTexture(mesh, textureData.uvCoordinatesType, textureData.projectionType, temporalMesh); |
||||
} |
||||
} |
||||
this.blend(resultTexture, textureData.textureBlender, blenderContext); |
||||
|
||||
previousTexture = resultTexture; |
||||
masterUVCoordinatesType = textureData.uvCoordinatesType; |
||||
} else { |
||||
if (textureData.texture instanceof GeneratedTexture) { |
||||
if (!(resultTexture instanceof TriangulatedTexture)) { |
||||
resultTexture = new TriangulatedTexture((Texture2D) resultTexture, resultUVS, blenderContext); |
||||
resultUVS = null; |
||||
previousTexture = resultTexture; |
||||
} |
||||
|
||||
TriangulatedTexture triangulatedTexture = ((GeneratedTexture) textureData.texture).triangulate(mesh, geometriesOMA, textureData.uvCoordinatesType, blenderContext); |
||||
triangulatedTexture.castToUVS((TriangulatedTexture) resultTexture, blenderContext); |
||||
triangulatedTexture.blend(textureData.textureBlender, (TriangulatedTexture) resultTexture, blenderContext); |
||||
resultTexture = previousTexture = triangulatedTexture; |
||||
} else if (textureData.texture instanceof Texture2D) { |
||||
if (this.isUVTypesMatch(masterUVCoordinatesType, masterUserUVSetName, textureData.uvCoordinatesType, textureData.uvCoordinatesName) && resultTexture instanceof Texture2D) { |
||||
this.scale((Texture2D) textureData.texture, resultTexture.getImage().getWidth(), resultTexture.getImage().getHeight()); |
||||
ImageUtils.merge(resultTexture.getImage(), textureData.texture.getImage()); |
||||
previousTexture = resultTexture; |
||||
} else { |
||||
if (!(resultTexture instanceof TriangulatedTexture)) { |
||||
resultTexture = new TriangulatedTexture((Texture2D) resultTexture, resultUVS, blenderContext); |
||||
resultUVS = null; |
||||
} |
||||
// first triangulate the current texture
|
||||
List<Vector2f> textureUVS = null; |
||||
if (textureData.uvCoordinatesType == UVCoordinatesType.TEXCO_UV && userDefinedUVCoordinates != null && userDefinedUVCoordinates.size() > 0) { |
||||
if (textureData.uvCoordinatesName == null) { |
||||
textureUVS = userDefinedUVCoordinates.values().iterator().next();// get the first UV available
|
||||
} else { |
||||
textureUVS = userDefinedUVCoordinates.get(textureData.uvCoordinatesName); |
||||
} |
||||
} else { |
||||
TemporalMesh geometries = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH); |
||||
textureUVS = UVCoordinatesGenerator.generateUVCoordinatesFor2DTexture(mesh, textureData.uvCoordinatesType, textureData.projectionType, geometries); |
||||
} |
||||
TriangulatedTexture triangulatedTexture = new TriangulatedTexture((Texture2D) textureData.texture, textureUVS, blenderContext); |
||||
// then move the texture to different UV's
|
||||
triangulatedTexture.castToUVS((TriangulatedTexture) resultTexture, blenderContext); |
||||
// merge triangulated textures
|
||||
for (int i = 0; i < ((TriangulatedTexture) resultTexture).getFaceTextureCount(); ++i) { |
||||
ImageUtils.merge(((TriangulatedTexture) resultTexture).getFaceTextureElement(i).image, triangulatedTexture.getFaceTextureElement(i).image); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (resultTexture instanceof TriangulatedTexture) { |
||||
if (mappingType == MaterialContext.MTEX_NOR) { |
||||
for (int i = 0; i < ((TriangulatedTexture) resultTexture).getFaceTextureCount(); ++i) { |
||||
TriangleTextureElement triangleTextureElement = ((TriangulatedTexture) resultTexture).getFaceTextureElement(i); |
||||
triangleTextureElement.image = ImageUtils.convertToNormalMapTexture(triangleTextureElement.image, 1);// TODO: get proper strength factor
|
||||
} |
||||
} |
||||
resultUVS = ((TriangulatedTexture) resultTexture).getResultUVS(); |
||||
resultTexture = ((TriangulatedTexture) resultTexture).getResultTexture(); |
||||
masterUserUVSetName = null; |
||||
} |
||||
|
||||
// setting additional data
|
||||
resultTexture.setWrap(WrapMode.Repeat); |
||||
// the filters are required if generated textures are used because
|
||||
// otherwise ugly lines appear between the mesh faces
|
||||
resultTexture.setMagFilter(MagFilter.Nearest); |
||||
resultTexture.setMinFilter(MinFilter.NearestNoMipMaps); |
||||
|
||||
return masterUserUVSetName; |
||||
} |
||||
|
||||
/** |
||||
* Generates a texture that will be used by the sky spatial. |
||||
* The result texture has 6 layers. Every image in each layer has equal size and its shape is a square. |
||||
* The size of each image is the maximum size (width or height) of the textures given. |
||||
* The default sky generated texture size is used (this value is set in the BlenderKey) if no picture textures |
||||
* are present or their sizes is lower than the generated texture size. |
||||
* The textures of lower sizes are properly scaled. |
||||
* All the textures are mixed into one and put as layers in the result texture. |
||||
* |
||||
* @param horizontalColor |
||||
* the horizon color |
||||
* @param zenithColor |
||||
* the zenith color |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return texture for the sky |
||||
*/ |
||||
public TextureCubeMap generateSkyTexture(ColorRGBA horizontalColor, ColorRGBA zenithColor, BlenderContext blenderContext) { |
||||
LOGGER.log(Level.FINE, "Preparing sky texture from {0} applied textures.", textureDatas.size()); |
||||
|
||||
LOGGER.fine("Computing the texture size."); |
||||
int size = -1; |
||||
for (TextureData textureData : textureDatas) { |
||||
if (textureData.texture instanceof Texture2D) { |
||||
size = Math.max(textureData.texture.getImage().getWidth(), size); |
||||
size = Math.max(textureData.texture.getImage().getHeight(), size); |
||||
} |
||||
} |
||||
if (size < 0) { |
||||
size = blenderContext.getBlenderKey().getSkyGeneratedTextureSize(); |
||||
} |
||||
LOGGER.log(Level.FINE, "The sky texture size will be: {0}x{0}.", size); |
||||
|
||||
TextureCubeMap result = null; |
||||
for (TextureData textureData : textureDatas) { |
||||
TextureCubeMap texture = null; |
||||
if (textureData.texture instanceof GeneratedTexture) { |
||||
texture = ((GeneratedTexture) textureData.texture).generateSkyTexture(size, horizontalColor, zenithColor, blenderContext); |
||||
} else { |
||||
// first create a grayscale version of the image
|
||||
Image image = textureData.texture.getImage(); |
||||
if (image.getWidth() != image.getHeight() || image.getWidth() != size) { |
||||
image = ImageUtils.resizeTo(image, size, size); |
||||
} |
||||
Image grayscaleImage = ImageUtils.convertToGrayscaleTexture(image); |
||||
|
||||
// add the sky colors to the image
|
||||
PixelInputOutput sourcePixelIO = PixelIOFactory.getPixelIO(grayscaleImage.getFormat()); |
||||
PixelInputOutput targetPixelIO = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
TexturePixel texturePixel = new TexturePixel(); |
||||
for (int x = 0; x < image.getWidth(); ++x) { |
||||
for (int y = 0; y < image.getHeight(); ++y) { |
||||
sourcePixelIO.read(grayscaleImage, 0, texturePixel, x, y); |
||||
texturePixel.intensity = texturePixel.red;// no matter which factor we use here, in grayscale they are all equal
|
||||
ImageUtils.color(texturePixel, horizontalColor, zenithColor); |
||||
targetPixelIO.write(image, 0, texturePixel, x, y); |
||||
} |
||||
} |
||||
|
||||
// create the cubemap texture from the coloured image
|
||||
ByteBuffer sourceData = image.getData(0); |
||||
ArrayList<ByteBuffer> data = new ArrayList<ByteBuffer>(6); |
||||
for (int i = 0; i < 6; ++i) { |
||||
data.add(BufferUtils.clone(sourceData)); |
||||
} |
||||
texture = new TextureCubeMap(new Image(image.getFormat(), image.getWidth(), image.getHeight(), 6, data, ColorSpace.Linear)); |
||||
} |
||||
|
||||
if (result == null) { |
||||
result = texture; |
||||
} else { |
||||
ImageUtils.mix(result.getImage(), texture.getImage()); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* The method checks if the texture UV coordinates match. |
||||
* It the types are equal and different then UVCoordinatesType.TEXCO_UV then we consider them a match. |
||||
* If they are both UVCoordinatesType.TEXCO_UV then they match only when their UV sets names are equal. |
||||
* In other cases they are considered NOT a match. |
||||
* @param type1 |
||||
* the UV coord type |
||||
* @param uvSetName1 |
||||
* the user's UV coords set name (considered only for UVCoordinatesType.TEXCO_UV) |
||||
* @param type2 |
||||
* the UV coord type |
||||
* @param uvSetName2 |
||||
* the user's UV coords set name (considered only for UVCoordinatesType.TEXCO_UV) |
||||
* @return <b>true</b> if the types match and <b>false</b> otherwise |
||||
*/ |
||||
private boolean isUVTypesMatch(UVCoordinatesType type1, String uvSetName1, UVCoordinatesType type2, String uvSetName2) { |
||||
if (type1 == type2) { |
||||
if (type1 == UVCoordinatesType.TEXCO_UV) { |
||||
if (uvSetName1 != null && uvSetName2 != null && uvSetName1.equals(uvSetName2)) { |
||||
return true; |
||||
} |
||||
} else { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* This method blends the texture. |
||||
* |
||||
* @param texture |
||||
* the texture to be blended |
||||
* @param textureBlender |
||||
* blending definition for the texture |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
private void blend(Texture texture, TextureBlender textureBlender, BlenderContext blenderContext) { |
||||
if (texture instanceof TriangulatedTexture) { |
||||
((TriangulatedTexture) texture).blend(textureBlender, null, blenderContext); |
||||
} else if (texture instanceof Texture2D) { |
||||
Image blendedImage = textureBlender.blend(texture.getImage(), null, blenderContext); |
||||
texture.setImage(blendedImage); |
||||
} else { |
||||
throw new IllegalArgumentException("Invalid type for texture to blend!"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return the result texture |
||||
*/ |
||||
public Texture getResultTexture() { |
||||
return resultTexture; |
||||
} |
||||
|
||||
/** |
||||
* @return the result UV coordinates |
||||
*/ |
||||
public List<Vector2f> getResultUVS() { |
||||
return resultUVS; |
||||
} |
||||
|
||||
/** |
||||
* @return the amount of added textures |
||||
*/ |
||||
public int getTexturesCount() { |
||||
return textureDatas.size(); |
||||
} |
||||
|
||||
/** |
||||
* @return the texture's mapping type |
||||
*/ |
||||
public int getMappingType() { |
||||
return mappingType; |
||||
} |
||||
|
||||
/** |
||||
* @return <b>true</b> if the texture has at least one generated texture component and <b>false</b> otherwise |
||||
*/ |
||||
public boolean hasGeneratedTextures() { |
||||
if (textureDatas != null) { |
||||
for (TextureData textureData : textureDatas) { |
||||
if (textureData.texture instanceof GeneratedTexture) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* This method determines if the given texture has no alpha channel. |
||||
* |
||||
* @param texture |
||||
* the texture to check for alpha channel |
||||
* @return <b>true</b> if the texture has no alpha channel and <b>false</b> |
||||
* otherwise |
||||
*/ |
||||
private boolean isWithoutAlpha(TextureData textureData, BlenderContext blenderContext) { |
||||
ColorBand colorBand = new ColorBand(textureData.textureStructure, blenderContext); |
||||
if (!colorBand.hasTransparencies()) { |
||||
int type = ((Number) textureData.textureStructure.getFieldValue("type")).intValue(); |
||||
if (type == TextureHelper.TEX_MAGIC) { |
||||
return true; |
||||
} |
||||
if (type == TextureHelper.TEX_VORONOI) { |
||||
int voronoiColorType = ((Number) textureData.textureStructure.getFieldValue("vn_coltype")).intValue(); |
||||
return voronoiColorType != 0;// voronoiColorType == 0:
|
||||
// intensity, voronoiColorType
|
||||
// != 0: col1, col2 or col3
|
||||
} |
||||
if (type == TextureHelper.TEX_CLOUDS) { |
||||
int sType = ((Number) textureData.textureStructure.getFieldValue("stype")).intValue(); |
||||
return sType == 1;// sType==0: without colors, sType==1: with
|
||||
// colors
|
||||
} |
||||
|
||||
// checking the flat textures for alpha values presence
|
||||
if (type == TextureHelper.TEX_IMAGE) { |
||||
Image image = textureData.texture.getImage(); |
||||
switch (image.getFormat()) { |
||||
case BGR8: |
||||
case DXT1: |
||||
case Luminance16F: |
||||
case Luminance32F: |
||||
case Luminance8: |
||||
case RGB111110F: |
||||
case RGB16F: |
||||
case RGB32F: |
||||
case RGB565: |
||||
case RGB8: |
||||
return true;// these types have no alpha by definition
|
||||
case ABGR8: |
||||
case DXT1A: |
||||
case DXT3: |
||||
case DXT5: |
||||
case Luminance16FAlpha16F: |
||||
case Luminance8Alpha8: |
||||
case RGBA16F: |
||||
case RGBA32F: |
||||
case RGBA8: |
||||
case ARGB8: |
||||
case BGRA8: |
||||
case RGB5A1:// with these types it is better to make sure if the texture is or is not transparent
|
||||
PixelInputOutput pixelInputOutput = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
int depth = image.getDepth() == 0 ? 1 : image.getDepth(); |
||||
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) { |
||||
for (int x = 0; x < image.getWidth(); ++x) { |
||||
for (int y = 0; y < image.getHeight(); ++y) { |
||||
pixelInputOutput.read(image, layerIndex, pixel, x, y); |
||||
if (pixel.alpha < 1.0f) { |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return true; |
||||
default: |
||||
throw new IllegalStateException("Unknown image format: " + image.getFormat()); |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* This method scales the given texture to the given size. |
||||
* |
||||
* @param texture |
||||
* the texture to be scaled |
||||
* @param width |
||||
* new width of the texture |
||||
* @param height |
||||
* new height of the texture |
||||
*/ |
||||
private void scale(Texture2D texture, int width, int height) { |
||||
// first determine if scaling is required
|
||||
boolean scaleRequired = texture.getImage().getWidth() != width || texture.getImage().getHeight() != height; |
||||
|
||||
if (scaleRequired) { |
||||
Image image = texture.getImage(); |
||||
BufferedImage sourceImage = ImageToAwt.convert(image, false, true, 0); |
||||
|
||||
int sourceWidth = sourceImage.getWidth(); |
||||
int sourceHeight = sourceImage.getHeight(); |
||||
|
||||
BufferedImage targetImage = new BufferedImage(width, height, sourceImage.getType()); |
||||
|
||||
Graphics2D g = targetImage.createGraphics(); |
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
||||
g.drawImage(sourceImage, 0, 0, width, height, 0, 0, sourceWidth, sourceHeight, null); |
||||
g.dispose(); |
||||
|
||||
Image output = new ImageLoader().load(targetImage, false); |
||||
image.setWidth(width); |
||||
image.setHeight(height); |
||||
image.setData(output.getData(0)); |
||||
image.setFormat(output.getFormat()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A simple class to aggregate the texture data (improves code quality). |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class TextureData { |
||||
/** The texture. */ |
||||
public Texture texture; |
||||
/** The texture blender (to mix the texture with its material color). */ |
||||
public TextureBlender textureBlender; |
||||
/** The type of UV coordinates. */ |
||||
public UVCoordinatesType uvCoordinatesType; |
||||
/** The type of UV coordinates projection (for flat textures). */ |
||||
public UVProjectionType projectionType; |
||||
/** The texture sructure. */ |
||||
public Structure textureStructure; |
||||
/** The name of the user's UV coordinates that are used for this texture. */ |
||||
public String uvCoordinatesName; |
||||
} |
||||
} |
@ -0,0 +1,157 @@ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.texture.Image.Format; |
||||
|
||||
/** |
||||
* The data that helps in bytes calculations for the result image. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class DDSTexelData { |
||||
/** The colors of the texes. */ |
||||
private TexturePixel[][] colors; |
||||
/** The indexes of the texels. */ |
||||
private long[] indexes; |
||||
/** The alphas of the texels (might be null). */ |
||||
private float[][] alphas; |
||||
/** The indexels of texels alpha values (might be null). */ |
||||
private long[] alphaIndexes; |
||||
/** The counter of texel x column. */ |
||||
private int xCounter; |
||||
/** The counter of texel y row. */ |
||||
private int yCounter; |
||||
/** The width of the image in pixels. */ |
||||
private int widthInPixels; |
||||
/** The height of the image in pixels. */ |
||||
private int heightInPixels; |
||||
/** The total texel count. */ |
||||
private int xTexelCount; |
||||
|
||||
/** |
||||
* Constructor. Allocates memory for data structures. |
||||
* |
||||
* @param compressedSize |
||||
* the size of compressed image (or its mipmap) |
||||
* @param widthToHeightRatio |
||||
* width/height ratio for the image |
||||
* @param format |
||||
* the format of the image |
||||
*/ |
||||
public DDSTexelData(int compressedSize, float widthToHeightRatio, Format format) { |
||||
int texelsCount = compressedSize * 8 / format.getBitsPerPixel() / 16; |
||||
this.colors = new TexturePixel[texelsCount][]; |
||||
this.indexes = new long[texelsCount]; |
||||
this.widthInPixels = (int) (0.5f * (float) Math.sqrt(this.getSizeInBytes() / widthToHeightRatio)); |
||||
this.heightInPixels = (int) (this.widthInPixels / widthToHeightRatio); |
||||
this.xTexelCount = widthInPixels >> 2; |
||||
this.yCounter = (heightInPixels >> 2) - 1;// xCounter is 0 for now
|
||||
if (format == Format.DXT3 || format == Format.DXT5) { |
||||
this.alphas = new float[texelsCount][]; |
||||
this.alphaIndexes = new long[texelsCount]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method adds a color and indexes for a texel. |
||||
* |
||||
* @param colors |
||||
* the colors of the texel |
||||
* @param indexes |
||||
* the indexes of the texel |
||||
*/ |
||||
public void add(TexturePixel[] colors, int indexes) { |
||||
this.add(colors, indexes, null, 0); |
||||
} |
||||
|
||||
/** |
||||
* This method adds a color, color indexes and alha values (with their |
||||
* indexes) for a texel. |
||||
* |
||||
* @param colors |
||||
* the colors of the texel |
||||
* @param indexes |
||||
* the indexes of the texel |
||||
* @param alphas |
||||
* the alpha values |
||||
* @param alphaIndexes |
||||
* the indexes of the given alpha values |
||||
*/ |
||||
public void add(TexturePixel[] colors, int indexes, float[] alphas, long alphaIndexes) { |
||||
int index = yCounter * xTexelCount + xCounter; |
||||
this.colors[index] = colors; |
||||
this.indexes[index] = indexes; |
||||
if (alphas != null) { |
||||
this.alphas[index] = alphas; |
||||
this.alphaIndexes[index] = alphaIndexes; |
||||
} |
||||
++this.xCounter; |
||||
if (this.xCounter >= this.xTexelCount) { |
||||
this.xCounter = 0; |
||||
--this.yCounter; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the values of the pixel located on the given |
||||
* coordinates on the result image. |
||||
* |
||||
* @param x |
||||
* the x coordinate of the pixel |
||||
* @param y |
||||
* the y coordinate of the pixel |
||||
* @param result |
||||
* the table where the result is stored |
||||
* @return <b>true</b> if the pixel was correctly read and <b>false</b> if |
||||
* the position was outside the image sizes |
||||
*/ |
||||
public boolean getRGBA8(int x, int y, byte[] result) { |
||||
int xTexetlIndex = x % widthInPixels / 4; |
||||
int yTexelIndex = y % heightInPixels / 4; |
||||
|
||||
int texelIndex = yTexelIndex * xTexelCount + xTexetlIndex; |
||||
if (texelIndex < colors.length) { |
||||
TexturePixel[] colors = this.colors[texelIndex]; |
||||
|
||||
// coordinates of the pixel in the selected texel
|
||||
x = x - 4 * xTexetlIndex;// pixels are arranged from left to right
|
||||
y = 3 - y - 4 * yTexelIndex;// pixels are arranged from bottom to top (that is why '3 - ...' is at the start)
|
||||
|
||||
int pixelIndexInTexel = (y * 4 + x) * (int) FastMath.log(colors.length, 2); |
||||
int alphaIndexInTexel = alphas != null ? (y * 4 + x) * (int) FastMath.log(alphas.length, 2) : 0; |
||||
|
||||
// getting the pixel
|
||||
int indexMask = colors.length - 1; |
||||
int colorIndex = (int) (this.indexes[texelIndex] >> pixelIndexInTexel & indexMask); |
||||
float alpha = this.alphas != null ? this.alphas[texelIndex][(int) (this.alphaIndexes[texelIndex] >> alphaIndexInTexel & 0x07)] : colors[colorIndex].alpha; |
||||
result[0] = (byte) (colors[colorIndex].red * 255.0f); |
||||
result[1] = (byte) (colors[colorIndex].green * 255.0f); |
||||
result[2] = (byte) (colors[colorIndex].blue * 255.0f); |
||||
result[3] = (byte) (alpha * 255.0f); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* @return the size of the decompressed texel (in bytes) |
||||
*/ |
||||
public int getSizeInBytes() { |
||||
// indexes.length == count of texels
|
||||
return indexes.length * 16 * 4; |
||||
} |
||||
|
||||
/** |
||||
* @return image (mipmap) width |
||||
*/ |
||||
public int getPixelWidth() { |
||||
return widthInPixels; |
||||
} |
||||
|
||||
/** |
||||
* @return image (mipmap) height |
||||
*/ |
||||
public int getPixelHeight() { |
||||
return heightInPixels; |
||||
} |
||||
} |
@ -0,0 +1,282 @@ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.TreeSet; |
||||
|
||||
import com.jme3.bounding.BoundingBox; |
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Mesh; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.meshes.TemporalMesh; |
||||
import com.jme3.scene.plugins.blender.textures.TriangulatedTexture.TriangleTextureElement; |
||||
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType; |
||||
import com.jme3.scene.plugins.blender.textures.generating.TextureGenerator; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Image.Format; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.texture.TextureCubeMap; |
||||
import com.jme3.util.TempVars; |
||||
|
||||
/** |
||||
* The generated texture loaded from blender file. The texture is not generated |
||||
* after being read. This class rather stores all required data and can compute |
||||
* a pixel in the required 3D space position. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class GeneratedTexture extends Texture { |
||||
private static final int POSITIVE_X = 0; |
||||
private static final int NEGATIVE_X = 1; |
||||
private static final int POSITIVE_Y = 2; |
||||
private static final int NEGATIVE_Y = 3; |
||||
private static final int POSITIVE_Z = 4; |
||||
private static final int NEGATIVE_Z = 5; |
||||
|
||||
// flag values
|
||||
public static final int TEX_COLORBAND = 1; |
||||
public static final int TEX_FLIPBLEND = 2; |
||||
public static final int TEX_NEGALPHA = 4; |
||||
public static final int TEX_CHECKER_ODD = 8; |
||||
public static final int TEX_CHECKER_EVEN = 16; |
||||
public static final int TEX_PRV_ALPHA = 32; |
||||
public static final int TEX_PRV_NOR = 64; |
||||
public static final int TEX_REPEAT_XMIR = 128; |
||||
public static final int TEX_REPEAT_YMIR = 256; |
||||
public static final int TEX_FLAG_MASK = TEX_COLORBAND | TEX_FLIPBLEND | TEX_NEGALPHA | TEX_CHECKER_ODD | TEX_CHECKER_EVEN | TEX_PRV_ALPHA | TEX_PRV_NOR | TEX_REPEAT_XMIR | TEX_REPEAT_YMIR; |
||||
|
||||
/** Material-texture link structure. */ |
||||
private final Structure mTex; |
||||
/** Texture generateo for the specified texture type. */ |
||||
private final TextureGenerator textureGenerator; |
||||
/** |
||||
* The generated texture cast functions. They are used to cas a given point on a plane to a specified shape in 3D space. |
||||
* The functions should be ordered as the ordinal of a BlenderKey.CastFunction enums. |
||||
*/ |
||||
private final static CastFunction[] CAST_FUNCTIONS = new CastFunction[] { |
||||
/** |
||||
* The cube casting function (does nothing except scaling if needed because the given points are already on a cube). |
||||
*/ |
||||
new CastFunction() { |
||||
@Override |
||||
public void cast(Vector3f pointToCast, float radius) { |
||||
//computed using the Thales' theorem
|
||||
float length = 2 * pointToCast.subtractLocal(0.5f, 0.5f, 0.5f).length() * radius; |
||||
pointToCast.normalizeLocal().addLocal(0.5f, 0.5f, 0.5f).multLocal(length); |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* The sphere casting function. |
||||
*/ |
||||
new CastFunction() { |
||||
/** |
||||
* The method casts a point on a plane to a sphere. |
||||
* The plane is one of the faces of a cube that has a edge of length 1 and center in (0.5 0.5, 0.5). This cube is a basic 3d area where generated texture |
||||
* is created. |
||||
* To cast a point on a cube face to a sphere that is inside the cube we perform several easy vector operations. |
||||
* 1. create a vector from the cube's center to the point |
||||
* 2. setting its length to 0.5 (the radius of the sphere) |
||||
* 3. adding the value of the cube's center to get a point on the sphere |
||||
* |
||||
* The result is stored in the given vector. |
||||
* |
||||
* @param pointToCast |
||||
* the point on a plane that will be cast to a sphere |
||||
* @param radius |
||||
* the radius of the sphere |
||||
*/ |
||||
@Override |
||||
public void cast(Vector3f pointToCast, float radius) { |
||||
pointToCast.subtractLocal(0.5f, 0.5f, 0.5f).normalizeLocal().multLocal(radius).addLocal(0.5f, 0.5f, 0.5f); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* Constructor. Reads the required data from the 'tex' structure. |
||||
* |
||||
* @param tex |
||||
* the texture structure |
||||
* @param mTex |
||||
* the material-texture link data structure |
||||
* @param textureGenerator |
||||
* the generator for the required texture type |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public GeneratedTexture(Structure tex, Structure mTex, TextureGenerator textureGenerator, BlenderContext blenderContext) { |
||||
this.mTex = mTex; |
||||
this.textureGenerator = textureGenerator; |
||||
this.textureGenerator.readData(tex, blenderContext); |
||||
super.setImage(new GeneratedTextureImage(textureGenerator.getImageFormat())); |
||||
} |
||||
|
||||
/** |
||||
* This method computes the textyre color/intensity at the specified (u, v, |
||||
* s) position in 3D space. |
||||
* |
||||
* @param pixel |
||||
* the pixel where the result is stored |
||||
* @param u |
||||
* the U factor |
||||
* @param v |
||||
* the V factor |
||||
* @param s |
||||
* the S factor |
||||
*/ |
||||
public void getPixel(TexturePixel pixel, float u, float v, float s) { |
||||
textureGenerator.getPixel(pixel, u, v, s); |
||||
} |
||||
|
||||
/** |
||||
* This method triangulates the texture. In the result we get a set of small |
||||
* flat textures for each face of the given mesh. This can be later merged |
||||
* into one flat texture. |
||||
* |
||||
* @param mesh |
||||
* the mesh we create the texture for |
||||
* @param geometriesOMA |
||||
* the old memory address of the geometries group that the given |
||||
* mesh belongs to (required for bounding box calculations) |
||||
* @param coordinatesType |
||||
* the types of UV coordinates |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return triangulated texture |
||||
*/ |
||||
public TriangulatedTexture triangulate(Mesh mesh, Long geometriesOMA, UVCoordinatesType coordinatesType, BlenderContext blenderContext) { |
||||
TemporalMesh geometries = (TemporalMesh) blenderContext.getLoadedFeature(geometriesOMA, LoadedDataType.TEMPORAL_MESH); |
||||
|
||||
int[] coordinatesSwappingIndexes = new int[] { ((Number) mTex.getFieldValue("projx")).intValue(), ((Number) mTex.getFieldValue("projy")).intValue(), ((Number) mTex.getFieldValue("projz")).intValue() }; |
||||
List<Vector3f> uvs = UVCoordinatesGenerator.generateUVCoordinatesFor3DTexture(mesh, coordinatesType, coordinatesSwappingIndexes, geometries); |
||||
Vector3f[] uvsArray = uvs.toArray(new Vector3f[uvs.size()]); |
||||
BoundingBox boundingBox = UVCoordinatesGenerator.getBoundingBox(geometries); |
||||
Set<TriangleTextureElement> triangleTextureElements = new TreeSet<TriangleTextureElement>(new Comparator<TriangleTextureElement>() { |
||||
@Override |
||||
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) { |
||||
return o1.faceIndex - o2.faceIndex; |
||||
} |
||||
}); |
||||
int[] indices = new int[3]; |
||||
for (int i = 0; i < mesh.getTriangleCount(); ++i) { |
||||
mesh.getTriangle(i, indices); |
||||
triangleTextureElements.add(new TriangleTextureElement(i, boundingBox, this, uvsArray, indices, blenderContext)); |
||||
} |
||||
return new TriangulatedTexture(triangleTextureElements, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* Creates a texture for the sky. The result texture has 6 layers. |
||||
* @param size |
||||
* the size of the texture (width and height are equal) |
||||
* @param horizontalColor |
||||
* the horizon color |
||||
* @param zenithColor |
||||
* the zenith color |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the sky texture |
||||
*/ |
||||
public TextureCubeMap generateSkyTexture(int size, ColorRGBA horizontalColor, ColorRGBA zenithColor, BlenderContext blenderContext) { |
||||
Image image = ImageUtils.createEmptyImage(Format.RGB8, size, size, 6); |
||||
PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
|
||||
float delta = 1 / (float) (size - 1); |
||||
float sideV, sideS = 1, forwardU = 1, forwardV, upS; |
||||
TempVars tempVars = TempVars.get(); |
||||
CastFunction castFunction = CAST_FUNCTIONS[blenderContext.getBlenderKey().getSkyGeneratedTextureShape().ordinal()]; |
||||
float castRadius = blenderContext.getBlenderKey().getSkyGeneratedTextureRadius(); |
||||
|
||||
for (int x = 0; x < size; ++x) { |
||||
sideV = 1; |
||||
forwardV = 1; |
||||
upS = 0; |
||||
for (int y = 0; y < size; ++y) { |
||||
castFunction.cast(tempVars.vect1.set(1, sideV, sideS), castRadius); |
||||
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z); |
||||
pixelIO.write(image, NEGATIVE_X, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// right
|
||||
|
||||
castFunction.cast(tempVars.vect1.set(0, sideV, 1 - sideS), castRadius); |
||||
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z); |
||||
pixelIO.write(image, POSITIVE_X, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// left
|
||||
|
||||
castFunction.cast(tempVars.vect1.set(forwardU, forwardV, 0), castRadius); |
||||
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z); |
||||
pixelIO.write(image, POSITIVE_Z, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// front
|
||||
|
||||
castFunction.cast(tempVars.vect1.set(1 - forwardU, forwardV, 1), castRadius); |
||||
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z); |
||||
pixelIO.write(image, NEGATIVE_Z, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// back
|
||||
|
||||
castFunction.cast(tempVars.vect1.set(forwardU, 0, upS), castRadius); |
||||
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z); |
||||
pixelIO.write(image, NEGATIVE_Y, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// top
|
||||
|
||||
castFunction.cast(tempVars.vect1.set(forwardU, 1, 1 - upS), castRadius); |
||||
textureGenerator.getPixel(pixel, tempVars.vect1.x, tempVars.vect1.y, tempVars.vect1.z); |
||||
pixelIO.write(image, POSITIVE_Y, ImageUtils.color(pixel, horizontalColor, zenithColor), x, y);// bottom
|
||||
|
||||
sideV = FastMath.clamp(sideV - delta, 0, 1); |
||||
forwardV = FastMath.clamp(forwardV - delta, 0, 1); |
||||
upS = FastMath.clamp(upS + delta, 0, 1); |
||||
} |
||||
sideS = FastMath.clamp(sideS - delta, 0, 1); |
||||
forwardU = FastMath.clamp(forwardU - delta, 0, 1); |
||||
} |
||||
tempVars.release(); |
||||
|
||||
return new TextureCubeMap(image); |
||||
} |
||||
|
||||
@Override |
||||
public void setWrap(WrapAxis axis, WrapMode mode) { |
||||
} |
||||
|
||||
@Override |
||||
public void setWrap(WrapMode mode) { |
||||
} |
||||
|
||||
@Override |
||||
public WrapMode getWrap(WrapAxis axis) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public Type getType() { |
||||
return Type.ThreeDimensional; |
||||
} |
||||
|
||||
@Override |
||||
public Texture createSimpleClone() { |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Private class to give the format of the 'virtual' 3D texture image. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class GeneratedTextureImage extends Image { |
||||
public GeneratedTextureImage(Format imageFormat) { |
||||
super.format = imageFormat; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The casting functions to create a sky generated texture against selected shape of a selected size. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static interface CastFunction { |
||||
void cast(Vector3f pointToCast, float radius); |
||||
} |
||||
} |
@ -0,0 +1,136 @@ |
||||
/* |
||||
* Copyright (c) 2009-2012 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import com.jme3.asset.AssetManager; |
||||
import com.jme3.asset.TextureKey; |
||||
import com.jme3.scene.plugins.blender.file.BlenderInputStream; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.texture.plugins.AWTLoader; |
||||
import com.jme3.texture.plugins.HDRLoader; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
/** |
||||
* An image loader class. It uses three loaders (AWTLoader, TGALoader and DDSLoader) in an attempt to load the image from the given |
||||
* input stream. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class ImageLoader extends AWTLoader { |
||||
private static final Logger LOGGER = Logger.getLogger(ImageLoader.class.getName()); |
||||
private static final Logger hdrLogger = Logger.getLogger(HDRLoader.class.getName()); // Used to silence HDR Errors
|
||||
|
||||
/** |
||||
* List of Blender-Supported Texture Extensions (we have to guess them, so |
||||
* the AssetLoader can find them. Not good, but better than nothing. |
||||
* Source: https://docs.blender.org/manual/en/dev/data_system/files/media/image_formats.html
|
||||
*/ |
||||
private static final String[] extensions = new String[] |
||||
{ /* Windows Bitmap */".bmp", |
||||
/* Iris */ ".sgi", ".rgb", ".bw", |
||||
/* PNG */ ".png", |
||||
/* JPEG */ ".jpg", ".jpeg", |
||||
/* JPEG 2000 */ ".jp2", ".j2c", |
||||
/* Targa */".tga", |
||||
/* Cineon & DPX */".cin", ".dpx", |
||||
/* OpenEXR */ ".exr", |
||||
/* Radiance HDR */ ".hdr", |
||||
/* TIFF */ ".tif", ".tiff", |
||||
/* DDS (Direct X) */ ".dds" }; |
||||
|
||||
/** |
||||
* This method loads a image which is packed into the blender file. |
||||
* It makes use of all the registered AssetLoaders |
||||
* |
||||
* @param inputStream |
||||
* blender input stream |
||||
* @param startPosition |
||||
* position in the stream where the image data starts |
||||
* @param flipY |
||||
* if the image should be flipped (does not work with DirectX image) |
||||
* @return loaded image or null if it could not be loaded |
||||
* @deprecated This method has only been left in for API compability. |
||||
* Use loadTexture instead |
||||
*/ |
||||
public Image loadImage(AssetManager assetManager, BlenderInputStream inputStream, int startPosition, boolean flipY) { |
||||
Texture tex = loadTexture(assetManager, inputStream, startPosition, flipY); |
||||
|
||||
if (tex == null) { |
||||
return null; |
||||
} else { |
||||
return tex.getImage(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method loads a texture which is packed into the blender file. |
||||
* It makes use of all the registered AssetLoaders |
||||
* |
||||
* @param inputStream |
||||
* blender input stream |
||||
* @param startPosition |
||||
* position in the stream where the image data starts |
||||
* @param flipY |
||||
* if the image should be flipped (does not work with DirectX image) |
||||
* @return loaded texture or null if it could not be loaded |
||||
*/ |
||||
public Texture loadTexture(AssetManager assetManager, BlenderInputStream inputStream, int startPosition, boolean flipY) { |
||||
inputStream.setPosition(startPosition); |
||||
TextureKey tKey; |
||||
Texture result = null; |
||||
|
||||
hdrLogger.setLevel(Level.SEVERE); // When we bruteforce try HDR on a non hdr file, it prints unreadable chars
|
||||
|
||||
for (String ext: extensions) { |
||||
tKey = new TextureKey("dummy" + ext, flipY); |
||||
try { |
||||
result = assetManager.loadAssetFromStream(tKey, inputStream); |
||||
} catch (Exception e) { |
||||
continue; |
||||
} |
||||
|
||||
if (result != null) { |
||||
break; // Could locate a possible asset
|
||||
} |
||||
} |
||||
|
||||
if (result == null) { |
||||
LOGGER.warning("Texture could not be loaded by any of the available loaders!\n" |
||||
+ "Since the file has been packed into the blender file, there is no" |
||||
+ "way for us to tell you which texture it was."); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,473 @@ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import java.awt.color.ColorSpace; |
||||
import java.awt.geom.AffineTransform; |
||||
import java.awt.image.AffineTransformOp; |
||||
import java.awt.image.BufferedImage; |
||||
import java.awt.image.ColorConvertOp; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import jme3tools.converters.ImageToAwt; |
||||
import jme3tools.converters.RGB565; |
||||
|
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Image.Format; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
/** |
||||
* This utility class has the methods that deal with images. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public final class ImageUtils { |
||||
/** |
||||
* Creates an image of the given size and depth. |
||||
* @param format |
||||
* the image format |
||||
* @param width |
||||
* the image width |
||||
* @param height |
||||
* the image height |
||||
* @param depth |
||||
* the image depth |
||||
* @return the new image instance |
||||
*/ |
||||
public static Image createEmptyImage(Format format, int width, int height, int depth) { |
||||
int bufferSize = width * height * (format.getBitsPerPixel() >> 3); |
||||
if (depth < 2) { |
||||
return new Image(format, width, height, BufferUtils.createByteBuffer(bufferSize), com.jme3.texture.image.ColorSpace.Linear); |
||||
} |
||||
ArrayList<ByteBuffer> data = new ArrayList<ByteBuffer>(depth); |
||||
for (int i = 0; i < depth; ++i) { |
||||
data.add(BufferUtils.createByteBuffer(bufferSize)); |
||||
} |
||||
return new Image(Format.RGB8, width, height, depth, data, com.jme3.texture.image.ColorSpace.Linear); |
||||
} |
||||
|
||||
/** |
||||
* The method sets a color for the given pixel by merging the two given colors. |
||||
* The lowIntensityColor will be most visible when the pixel has low intensity. |
||||
* The highIntensityColor will be most visible when the pixel has high intensity. |
||||
* |
||||
* @param pixel |
||||
* the pixel that will have the colors altered |
||||
* @param lowIntensityColor |
||||
* the low intensity color |
||||
* @param highIntensityColor |
||||
* the high intensity color |
||||
* @return the altered pixel (the same instance) |
||||
*/ |
||||
public static TexturePixel color(TexturePixel pixel, ColorRGBA lowIntensityColor, ColorRGBA highIntensityColor) { |
||||
float intensity = pixel.intensity; |
||||
pixel.fromColor(lowIntensityColor); |
||||
pixel.mult(1 - pixel.intensity); |
||||
pixel.add(highIntensityColor.mult(intensity)); |
||||
return pixel; |
||||
} |
||||
|
||||
/** |
||||
* This method merges two given images. The result is stored in the |
||||
* 'target' image. |
||||
* |
||||
* @param targetImage |
||||
* the target image |
||||
* @param sourceImage |
||||
* the source image |
||||
*/ |
||||
public static void merge(Image targetImage, Image sourceImage) { |
||||
if (sourceImage.getDepth() != targetImage.getDepth()) { |
||||
throw new IllegalArgumentException("The given images should have the same depth to merge them!"); |
||||
} |
||||
if (sourceImage.getWidth() != targetImage.getWidth()) { |
||||
throw new IllegalArgumentException("The given images should have the same width to merge them!"); |
||||
} |
||||
if (sourceImage.getHeight() != targetImage.getHeight()) { |
||||
throw new IllegalArgumentException("The given images should have the same height to merge them!"); |
||||
} |
||||
|
||||
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(sourceImage.getFormat()); |
||||
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(targetImage.getFormat()); |
||||
TexturePixel sourcePixel = new TexturePixel(); |
||||
TexturePixel targetPixel = new TexturePixel(); |
||||
int depth = targetImage.getDepth() == 0 ? 1 : targetImage.getDepth(); |
||||
|
||||
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) { |
||||
for (int x = 0; x < sourceImage.getWidth(); ++x) { |
||||
for (int y = 0; y < sourceImage.getHeight(); ++y) { |
||||
sourceIO.read(sourceImage, layerIndex, sourcePixel, x, y); |
||||
targetIO.read(targetImage, layerIndex, targetPixel, x, y); |
||||
targetPixel.merge(sourcePixel); |
||||
targetIO.write(targetImage, layerIndex, targetPixel, x, y); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method merges two given images. The result is stored in the |
||||
* 'target' image. |
||||
* |
||||
* @param targetImage |
||||
* the target image |
||||
* @param sourceImage |
||||
* the source image |
||||
*/ |
||||
public static void mix(Image targetImage, Image sourceImage) { |
||||
if (sourceImage.getDepth() != targetImage.getDepth()) { |
||||
throw new IllegalArgumentException("The given images should have the same depth to merge them!"); |
||||
} |
||||
if (sourceImage.getWidth() != targetImage.getWidth()) { |
||||
throw new IllegalArgumentException("The given images should have the same width to merge them!"); |
||||
} |
||||
if (sourceImage.getHeight() != targetImage.getHeight()) { |
||||
throw new IllegalArgumentException("The given images should have the same height to merge them!"); |
||||
} |
||||
|
||||
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(sourceImage.getFormat()); |
||||
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(targetImage.getFormat()); |
||||
TexturePixel sourcePixel = new TexturePixel(); |
||||
TexturePixel targetPixel = new TexturePixel(); |
||||
int depth = targetImage.getDepth() == 0 ? 1 : targetImage.getDepth(); |
||||
|
||||
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) { |
||||
for (int x = 0; x < sourceImage.getWidth(); ++x) { |
||||
for (int y = 0; y < sourceImage.getHeight(); ++y) { |
||||
sourceIO.read(sourceImage, layerIndex, sourcePixel, x, y); |
||||
targetIO.read(targetImage, layerIndex, targetPixel, x, y); |
||||
targetPixel.mix(sourcePixel); |
||||
targetIO.write(targetImage, layerIndex, targetPixel, x, y); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Resizes the image to the given width and height. |
||||
* @param source |
||||
* the source image (this remains untouched, the new image instance is created) |
||||
* @param width |
||||
* the target image width |
||||
* @param height |
||||
* the target image height |
||||
* @return the resized image |
||||
*/ |
||||
public static Image resizeTo(Image source, int width, int height) { |
||||
BufferedImage sourceImage = ImageToAwt.convert(source, false, false, 0); |
||||
|
||||
double scaleX = width / (double) sourceImage.getWidth(); |
||||
double scaleY = height / (double) sourceImage.getHeight(); |
||||
AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY); |
||||
AffineTransformOp bilinearScaleOp = new AffineTransformOp(scaleTransform, AffineTransformOp.TYPE_BILINEAR); |
||||
|
||||
BufferedImage scaledImage = bilinearScaleOp.filter(sourceImage, new BufferedImage(width, height, sourceImage.getType())); |
||||
return ImageUtils.toJmeImage(scaledImage, source.getFormat()); |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given texture into normal-map texture. |
||||
* |
||||
* @param source |
||||
* the source texture |
||||
* @param strengthFactor |
||||
* the normal strength factor |
||||
* @return normal-map texture |
||||
*/ |
||||
public static Image convertToNormalMapTexture(Image source, float strengthFactor) { |
||||
BufferedImage sourceImage = ImageToAwt.convert(source, false, false, 0); |
||||
|
||||
BufferedImage heightMap = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_ARGB); |
||||
BufferedImage bumpMap = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_ARGB); |
||||
ColorConvertOp gscale = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); |
||||
gscale.filter(sourceImage, heightMap); |
||||
|
||||
Vector3f S = new Vector3f(); |
||||
Vector3f T = new Vector3f(); |
||||
Vector3f N = new Vector3f(); |
||||
|
||||
for (int x = 0; x < bumpMap.getWidth(); ++x) { |
||||
for (int y = 0; y < bumpMap.getHeight(); ++y) { |
||||
// generating bump pixel
|
||||
S.x = 1; |
||||
S.y = 0; |
||||
S.z = strengthFactor * ImageUtils.getHeight(heightMap, x + 1, y) - strengthFactor * ImageUtils.getHeight(heightMap, x - 1, y); |
||||
T.x = 0; |
||||
T.y = 1; |
||||
T.z = strengthFactor * ImageUtils.getHeight(heightMap, x, y + 1) - strengthFactor * ImageUtils.getHeight(heightMap, x, y - 1); |
||||
|
||||
float den = (float) Math.sqrt(S.z * S.z + T.z * T.z + 1); |
||||
N.x = -S.z; |
||||
N.y = -T.z; |
||||
N.z = 1; |
||||
N.divideLocal(den); |
||||
|
||||
// setting the pixel in the result image
|
||||
bumpMap.setRGB(x, y, ImageUtils.vectorToColor(N.x, N.y, N.z)); |
||||
} |
||||
} |
||||
return ImageUtils.toJmeImage(bumpMap, source.getFormat()); |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given texture into black and whit (grayscale) texture. |
||||
* |
||||
* @param source |
||||
* the source texture |
||||
* @return grayscale texture |
||||
*/ |
||||
public static Image convertToGrayscaleTexture(Image source) { |
||||
BufferedImage sourceImage = ImageToAwt.convert(source, false, false, 0); |
||||
ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); |
||||
op.filter(sourceImage, sourceImage); |
||||
return ImageUtils.toJmeImage(sourceImage, source.getFormat()); |
||||
} |
||||
|
||||
/** |
||||
* This method decompresses the given image. If the given image is already |
||||
* decompressed nothing happens and it is simply returned. |
||||
* |
||||
* @param image |
||||
* the image to decompress |
||||
* @return the decompressed image |
||||
*/ |
||||
public static Image decompress(Image image) { |
||||
Format format = image.getFormat(); |
||||
int depth = image.getDepth(); |
||||
if (depth == 0) { |
||||
depth = 1; |
||||
} |
||||
ArrayList<ByteBuffer> dataArray = new ArrayList<ByteBuffer>(depth); |
||||
int[] sizes = image.getMipMapSizes() != null ? image.getMipMapSizes() : new int[1]; |
||||
int[] newMipmapSizes = image.getMipMapSizes() != null ? new int[image.getMipMapSizes().length] : null; |
||||
|
||||
for (int dataLayerIndex = 0; dataLayerIndex < depth; ++dataLayerIndex) { |
||||
ByteBuffer data = image.getData(dataLayerIndex); |
||||
data.rewind(); |
||||
if (sizes.length == 1) { |
||||
sizes[0] = data.remaining(); |
||||
} |
||||
float widthToHeightRatio = image.getWidth() / image.getHeight();// this should always be constant for each mipmap
|
||||
List<DDSTexelData> texelDataList = new ArrayList<DDSTexelData>(sizes.length); |
||||
int maxPosition = 0, resultSize = 0; |
||||
|
||||
for (int sizeIndex = 0; sizeIndex < sizes.length; ++sizeIndex) { |
||||
maxPosition += sizes[sizeIndex]; |
||||
DDSTexelData texelData = new DDSTexelData(sizes[sizeIndex], widthToHeightRatio, format); |
||||
texelDataList.add(texelData); |
||||
switch (format) { |
||||
case DXT1:// BC1
|
||||
case DXT1A: |
||||
while (data.position() < maxPosition) { |
||||
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel(), new TexturePixel(), new TexturePixel() }; |
||||
short c0 = data.getShort(); |
||||
short c1 = data.getShort(); |
||||
int col0 = RGB565.RGB565_to_ARGB8(c0); |
||||
int col1 = RGB565.RGB565_to_ARGB8(c1); |
||||
colors[0].fromARGB8(col0); |
||||
colors[1].fromARGB8(col1); |
||||
|
||||
if (col0 > col1) { |
||||
// creating color2 = 2/3color0 + 1/3color1
|
||||
colors[2].fromPixel(colors[0]); |
||||
colors[2].mult(2); |
||||
colors[2].add(colors[1]); |
||||
colors[2].divide(3); |
||||
|
||||
// creating color3 = 1/3color0 + 2/3color1;
|
||||
colors[3].fromPixel(colors[1]); |
||||
colors[3].mult(2); |
||||
colors[3].add(colors[0]); |
||||
colors[3].divide(3); |
||||
} else { |
||||
// creating color2 = 1/2color0 + 1/2color1
|
||||
colors[2].fromPixel(colors[0]); |
||||
colors[2].add(colors[1]); |
||||
colors[2].mult(0.5f); |
||||
|
||||
colors[3].fromARGB8(0); |
||||
} |
||||
int indexes = data.getInt();// 4-byte table with color indexes in decompressed table
|
||||
texelData.add(colors, indexes); |
||||
} |
||||
break; |
||||
case DXT3:// BC2
|
||||
while (data.position() < maxPosition) { |
||||
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel(), new TexturePixel(), new TexturePixel() }; |
||||
long alpha = data.getLong(); |
||||
float[] alphas = new float[16]; |
||||
long alphasIndex = 0; |
||||
for (int i = 0; i < 16; ++i) { |
||||
alphasIndex |= i << i * 4; |
||||
byte a = (byte) ((alpha >> i * 4 & 0x0F) << 4); |
||||
alphas[i] = a >= 0 ? a / 255.0f : 1.0f - ~a / 255.0f; |
||||
} |
||||
|
||||
short c0 = data.getShort(); |
||||
short c1 = data.getShort(); |
||||
int col0 = RGB565.RGB565_to_ARGB8(c0); |
||||
int col1 = RGB565.RGB565_to_ARGB8(c1); |
||||
colors[0].fromARGB8(col0); |
||||
colors[1].fromARGB8(col1); |
||||
|
||||
// creating color2 = 2/3color0 + 1/3color1
|
||||
colors[2].fromPixel(colors[0]); |
||||
colors[2].mult(2); |
||||
colors[2].add(colors[1]); |
||||
colors[2].divide(3); |
||||
|
||||
// creating color3 = 1/3color0 + 2/3color1;
|
||||
colors[3].fromPixel(colors[1]); |
||||
colors[3].mult(2); |
||||
colors[3].add(colors[0]); |
||||
colors[3].divide(3); |
||||
|
||||
int indexes = data.getInt();// 4-byte table with color indexes in decompressed table
|
||||
texelData.add(colors, indexes, alphas, alphasIndex); |
||||
} |
||||
break; |
||||
case DXT5:// BC3
|
||||
float[] alphas = new float[8]; |
||||
while (data.position() < maxPosition) { |
||||
TexturePixel[] colors = new TexturePixel[] { new TexturePixel(), new TexturePixel(), new TexturePixel(), new TexturePixel() }; |
||||
alphas[0] = data.get() * 255.0f; |
||||
alphas[1] = data.get() * 255.0f; |
||||
//the casts to long must be done here because otherwise 32-bit integers would be shifetd by 32 and 40 bits which would result in improper values
|
||||
long alphaIndices = data.get() | (long)data.get() << 8 | (long)data.get() << 16 | (long)data.get() << 24 | (long)data.get() << 32 | (long)data.get() << 40; |
||||
if (alphas[0] > alphas[1]) {// 6 interpolated alpha values.
|
||||
alphas[2] = (6 * alphas[0] + alphas[1]) / 7; |
||||
alphas[3] = (5 * alphas[0] + 2 * alphas[1]) / 7; |
||||
alphas[4] = (4 * alphas[0] + 3 * alphas[1]) / 7; |
||||
alphas[5] = (3 * alphas[0] + 4 * alphas[1]) / 7; |
||||
alphas[6] = (2 * alphas[0] + 5 * alphas[1]) / 7; |
||||
alphas[7] = (alphas[0] + 6 * alphas[1]) / 7; |
||||
} else { |
||||
alphas[2] = (4 * alphas[0] + alphas[1]) * 0.2f; |
||||
alphas[3] = (3 * alphas[0] + 2 * alphas[1]) * 0.2f; |
||||
alphas[4] = (2 * alphas[0] + 3 * alphas[1]) * 0.2f; |
||||
alphas[5] = (alphas[0] + 4 * alphas[1]) * 0.2f; |
||||
alphas[6] = 0; |
||||
alphas[7] = 1; |
||||
} |
||||
|
||||
short c0 = data.getShort(); |
||||
short c1 = data.getShort(); |
||||
int col0 = RGB565.RGB565_to_ARGB8(c0); |
||||
int col1 = RGB565.RGB565_to_ARGB8(c1); |
||||
colors[0].fromARGB8(col0); |
||||
colors[1].fromARGB8(col1); |
||||
|
||||
// creating color2 = 2/3color0 + 1/3color1
|
||||
colors[2].fromPixel(colors[0]); |
||||
colors[2].mult(2); |
||||
colors[2].add(colors[1]); |
||||
colors[2].divide(3); |
||||
|
||||
// creating color3 = 1/3color0 + 2/3color1;
|
||||
colors[3].fromPixel(colors[1]); |
||||
colors[3].mult(2); |
||||
colors[3].add(colors[0]); |
||||
colors[3].divide(3); |
||||
|
||||
int indexes = data.getInt();// 4-byte table with color indexes in decompressed table
|
||||
texelData.add(colors, indexes, alphas, alphaIndices); |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown compressed format: " + format); |
||||
} |
||||
newMipmapSizes[sizeIndex] = texelData.getSizeInBytes(); |
||||
resultSize += texelData.getSizeInBytes(); |
||||
} |
||||
byte[] bytes = new byte[resultSize]; |
||||
int offset = 0; |
||||
byte[] pixelBytes = new byte[4]; |
||||
for (DDSTexelData texelData : texelDataList) { |
||||
for (int i = 0; i < texelData.getPixelWidth(); ++i) { |
||||
for (int j = 0; j < texelData.getPixelHeight(); ++j) { |
||||
if (texelData.getRGBA8(i, j, pixelBytes)) { |
||||
bytes[offset + (j * texelData.getPixelWidth() + i) * 4] = pixelBytes[0]; |
||||
bytes[offset + (j * texelData.getPixelWidth() + i) * 4 + 1] = pixelBytes[1]; |
||||
bytes[offset + (j * texelData.getPixelWidth() + i) * 4 + 2] = pixelBytes[2]; |
||||
bytes[offset + (j * texelData.getPixelWidth() + i) * 4 + 3] = pixelBytes[3]; |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
offset += texelData.getSizeInBytes(); |
||||
} |
||||
dataArray.add(BufferUtils.createByteBuffer(bytes)); |
||||
} |
||||
|
||||
Image result = depth > 1 ? new Image(Format.RGBA8, image.getWidth(), image.getHeight(), depth, dataArray, com.jme3.texture.image.ColorSpace.Linear) : |
||||
new Image(Format.RGBA8, image.getWidth(), image.getHeight(), dataArray.get(0), com.jme3.texture.image.ColorSpace.Linear); |
||||
if (newMipmapSizes != null) { |
||||
result.setMipMapSizes(newMipmapSizes); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method returns the height represented by the specified pixel in the |
||||
* given texture. The given texture should be a height-map. |
||||
* |
||||
* @param image |
||||
* the height-map texture |
||||
* @param x |
||||
* pixel's X coordinate |
||||
* @param y |
||||
* pixel's Y coordinate |
||||
* @return height represented by the given texture in the specified location |
||||
*/ |
||||
private static int getHeight(BufferedImage image, int x, int y) { |
||||
if (x < 0) { |
||||
x = 0; |
||||
} else if (x >= image.getWidth()) { |
||||
x = image.getWidth() - 1; |
||||
} |
||||
if (y < 0) { |
||||
y = 0; |
||||
} else if (y >= image.getHeight()) { |
||||
y = image.getHeight() - 1; |
||||
} |
||||
return image.getRGB(x, y) & 0xff; |
||||
} |
||||
|
||||
/** |
||||
* This method transforms given vector's coordinates into ARGB color (A is |
||||
* always = 255). |
||||
* |
||||
* @param x |
||||
* X factor of the vector |
||||
* @param y |
||||
* Y factor of the vector |
||||
* @param z |
||||
* Z factor of the vector |
||||
* @return color representation of the given vector |
||||
*/ |
||||
private static int vectorToColor(float x, float y, float z) { |
||||
int r = Math.round(255 * (x + 1f) / 2f); |
||||
int g = Math.round(255 * (y + 1f) / 2f); |
||||
int b = Math.round(255 * (z + 1f) / 2f); |
||||
return (255 << 24) + (r << 16) + (g << 8) + b; |
||||
} |
||||
|
||||
/** |
||||
* Converts java awt image to jme image. |
||||
* @param bufferedImage |
||||
* the java awt image |
||||
* @param format |
||||
* the result image format |
||||
* @return the jme image |
||||
*/ |
||||
private static Image toJmeImage(BufferedImage bufferedImage, Format format) { |
||||
ByteBuffer byteBuffer = BufferUtils.createByteBuffer(bufferedImage.getWidth() * bufferedImage.getHeight() * 3); |
||||
ImageToAwt.convert(bufferedImage, format, byteBuffer); |
||||
return new Image(format, bufferedImage.getWidth(), bufferedImage.getHeight(), byteBuffer, com.jme3.texture.image.ColorSpace.Linear); |
||||
} |
||||
} |
@ -0,0 +1,691 @@ |
||||
/* |
||||
* Copyright (c) 2009-2018 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import java.awt.geom.AffineTransform; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.asset.AssetInfo; |
||||
import com.jme3.asset.AssetLoadException; |
||||
import com.jme3.asset.AssetManager; |
||||
import com.jme3.asset.AssetNotFoundException; |
||||
import com.jme3.asset.BlenderKey; |
||||
import com.jme3.asset.GeneratedTextureKey; |
||||
import com.jme3.asset.TextureKey; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.scene.VertexBuffer.Type; |
||||
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
import com.jme3.scene.plugins.blender.file.DynamicArray; |
||||
import com.jme3.scene.plugins.blender.file.FileBlockHeader; |
||||
import com.jme3.scene.plugins.blender.file.Pointer; |
||||
import com.jme3.scene.plugins.blender.file.Structure; |
||||
import com.jme3.scene.plugins.blender.materials.MaterialContext; |
||||
import com.jme3.scene.plugins.blender.textures.UVCoordinatesGenerator.UVCoordinatesType; |
||||
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender; |
||||
import com.jme3.scene.plugins.blender.textures.blending.TextureBlenderFactory; |
||||
import com.jme3.scene.plugins.blender.textures.generating.TextureGeneratorFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.texture.Texture.MinFilter; |
||||
import com.jme3.texture.Texture.WrapMode; |
||||
import com.jme3.texture.Texture2D; |
||||
import com.jme3.texture.image.ColorSpace; |
||||
import com.jme3.util.BufferUtils; |
||||
import com.jme3.util.PlaceholderAssets; |
||||
|
||||
/** |
||||
* A class that is used in texture calculations. |
||||
* |
||||
* @author Marcin Roguski |
||||
*/ |
||||
public class TextureHelper extends AbstractBlenderHelper { |
||||
private static final Logger LOGGER = Logger.getLogger(TextureHelper.class.getName()); |
||||
|
||||
// texture types
|
||||
public static final int TEX_NONE = 0; |
||||
public static final int TEX_CLOUDS = 1; |
||||
public static final int TEX_WOOD = 2; |
||||
public static final int TEX_MARBLE = 3; |
||||
public static final int TEX_MAGIC = 4; |
||||
public static final int TEX_BLEND = 5; |
||||
public static final int TEX_STUCCI = 6; |
||||
public static final int TEX_NOISE = 7; |
||||
public static final int TEX_IMAGE = 8; |
||||
public static final int TEX_PLUGIN = 9; |
||||
public static final int TEX_ENVMAP = 10; |
||||
public static final int TEX_MUSGRAVE = 11; |
||||
public static final int TEX_VORONOI = 12; |
||||
public static final int TEX_DISTNOISE = 13; |
||||
public static final int TEX_POINTDENSITY = 14; // v. 25+
|
||||
public static final int TEX_VOXELDATA = 15; // v. 25+
|
||||
public static final int TEX_OCEAN = 16; // v. 26+
|
||||
|
||||
public static final Type[] TEXCOORD_TYPES = new Type[] { Type.TexCoord, Type.TexCoord2, Type.TexCoord3, Type.TexCoord4, Type.TexCoord5, Type.TexCoord6, Type.TexCoord7, Type.TexCoord8 }; |
||||
|
||||
private TextureGeneratorFactory textureGeneratorFactory = new TextureGeneratorFactory(); |
||||
|
||||
/** |
||||
* This constructor parses the given blender version and stores the result. |
||||
* It creates noise generator and texture generators. |
||||
* |
||||
* @param blenderVersion |
||||
* the version read from the blend file |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public TextureHelper(String blenderVersion, BlenderContext blenderContext) { |
||||
super(blenderVersion, blenderContext); |
||||
} |
||||
|
||||
/** |
||||
* This class returns a texture read from the file or from packed blender |
||||
* data. The returned texture has the name set to the value of its blender |
||||
* type. |
||||
* |
||||
* @param textureStructure |
||||
* texture structure filled with data |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the texture that can be used by JME engine |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is |
||||
* somehow invalid or corrupted |
||||
*/ |
||||
public Texture getTexture(Structure textureStructure, Structure mTex, BlenderContext blenderContext) throws BlenderFileException { |
||||
Texture result = (Texture) blenderContext.getLoadedFeature(textureStructure.getOldMemoryAddress(), LoadedDataType.FEATURE); |
||||
if (result != null) { |
||||
return result; |
||||
} |
||||
|
||||
if ("ID".equals(textureStructure.getType())) { |
||||
LOGGER.fine("Loading texture from external blend file."); |
||||
return (Texture) this.loadLibrary(textureStructure); |
||||
} |
||||
|
||||
int type = ((Number) textureStructure.getFieldValue("type")).intValue(); |
||||
int imaflag = ((Number) textureStructure.getFieldValue("imaflag")).intValue(); |
||||
|
||||
switch (type) { |
||||
case TEX_IMAGE:// (it is first because probably this will be most commonly used)
|
||||
Pointer pImage = (Pointer) textureStructure.getFieldValue("ima"); |
||||
if (pImage.isNotNull()) { |
||||
Structure image = pImage.fetchData().get(0); |
||||
Texture loadedTexture = this.loadImageAsTexture(image, imaflag, blenderContext); |
||||
if (loadedTexture != null) { |
||||
result = loadedTexture; |
||||
this.applyColorbandAndColorFactors(textureStructure, result.getImage(), blenderContext); |
||||
} |
||||
} |
||||
break; |
||||
case TEX_CLOUDS: |
||||
case TEX_WOOD: |
||||
case TEX_MARBLE: |
||||
case TEX_MAGIC: |
||||
case TEX_BLEND: |
||||
case TEX_STUCCI: |
||||
case TEX_NOISE: |
||||
case TEX_MUSGRAVE: |
||||
case TEX_VORONOI: |
||||
case TEX_DISTNOISE: |
||||
result = new GeneratedTexture(textureStructure, mTex, textureGeneratorFactory.createTextureGenerator(type), blenderContext); |
||||
break; |
||||
case TEX_NONE:// No texture, do nothing
|
||||
break; |
||||
case TEX_POINTDENSITY: |
||||
case TEX_VOXELDATA: |
||||
case TEX_PLUGIN: |
||||
case TEX_ENVMAP: |
||||
case TEX_OCEAN: |
||||
LOGGER.log(Level.WARNING, "Unsupported texture type: {0} for texture: {1}", new Object[] { type, textureStructure.getName() }); |
||||
break; |
||||
default: |
||||
throw new BlenderFileException("Unknown texture type: " + type + " for texture: " + textureStructure.getName()); |
||||
} |
||||
if (result != null) { |
||||
result.setName(textureStructure.getName()); |
||||
result.setWrap(WrapMode.Repeat); |
||||
|
||||
// decide if the mipmaps will be generated
|
||||
switch (blenderContext.getBlenderKey().getMipmapGenerationMethod()) { |
||||
case ALWAYS_GENERATE: |
||||
result.setMinFilter(MinFilter.Trilinear); |
||||
break; |
||||
case NEVER_GENERATE: |
||||
break; |
||||
case GENERATE_WHEN_NEEDED: |
||||
if ((imaflag & 0x04) != 0) { |
||||
result.setMinFilter(MinFilter.Trilinear); |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown mipmap generation method: " + blenderContext.getBlenderKey().getMipmapGenerationMethod()); |
||||
} |
||||
|
||||
if (type != TEX_IMAGE) {// only generated textures should have this key
|
||||
result.setKey(new GeneratedTextureKey(textureStructure.getName())); |
||||
} |
||||
|
||||
if (LOGGER.isLoggable(Level.FINE)) { |
||||
LOGGER.log(Level.FINE, "Adding texture {0} to the loaded features with OMA = {1}", new Object[] { result.getName(), textureStructure.getOldMemoryAddress() }); |
||||
} |
||||
blenderContext.addLoadedFeatures(textureStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, textureStructure); |
||||
blenderContext.addLoadedFeatures(textureStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This class returns a texture read from the file or from packed blender |
||||
* data. |
||||
* |
||||
* @param imageStructure |
||||
* image structure filled with data |
||||
* @param imaflag |
||||
* the image flag |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the texture that can be used by JME engine |
||||
* @throws BlenderFileException |
||||
* this exception is thrown when the blend file structure is |
||||
* somehow invalid or corrupted |
||||
*/ |
||||
public Texture loadImageAsTexture(Structure imageStructure, int imaflag, BlenderContext blenderContext) throws BlenderFileException { |
||||
LOGGER.log(Level.FINE, "Fetching texture with OMA = {0}", imageStructure.getOldMemoryAddress()); |
||||
Texture result = null; |
||||
Image im = (Image) blenderContext.getLoadedFeature(imageStructure.getOldMemoryAddress(), LoadedDataType.FEATURE); |
||||
// if (im == null) { HACK force reaload always, as constructor in else case is destroying the TextureKeys!
|
||||
if ("ID".equals(imageStructure.getType())) { |
||||
LOGGER.fine("Loading texture from external blend file."); |
||||
result = (Texture) this.loadLibrary(imageStructure); |
||||
} else { |
||||
String texturePath = imageStructure.getFieldValue("name").toString(); |
||||
Pointer pPackedFile = (Pointer) imageStructure.getFieldValue("packedfile"); |
||||
if (pPackedFile.isNull()) { |
||||
LOGGER.log(Level.FINE, "Reading texture from file: {0}", texturePath); |
||||
result = this.loadImageFromFile(texturePath, imaflag, blenderContext); |
||||
} else { |
||||
LOGGER.fine("Packed texture. Reading directly from the blend file!"); |
||||
Structure packedFile = pPackedFile.fetchData().get(0); |
||||
Pointer pData = (Pointer) packedFile.getFieldValue("data"); |
||||
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pData.getOldMemoryAddress()); |
||||
blenderContext.getInputStream().setPosition(dataFileBlock.getBlockPosition()); |
||||
|
||||
// Should the texture be flipped? It works for sinbad ..
|
||||
result = new ImageLoader().loadTexture(blenderContext.getAssetManager(), blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true); |
||||
if (result == null) { |
||||
result = new Texture2D(PlaceholderAssets.getPlaceholderImage(blenderContext.getAssetManager())); |
||||
LOGGER.fine("ImageLoader returned null. It probably failed to load the packed texture, using placeholder asset"); |
||||
} |
||||
} |
||||
} |
||||
//} else {
|
||||
// result = new Texture2D(im);
|
||||
// }
|
||||
|
||||
if (result != null) {// render result is not being loaded
|
||||
blenderContext.addLoadedFeatures(imageStructure.getOldMemoryAddress(), LoadedDataType.STRUCTURE, imageStructure); |
||||
blenderContext.addLoadedFeatures(imageStructure.getOldMemoryAddress(), LoadedDataType.FEATURE, result.getImage()); |
||||
result.setName(imageStructure.getName()); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method creates the affine transform that is used to transform a |
||||
* triangle defined by one UV coordinates into a triangle defined by |
||||
* different UV's. |
||||
* |
||||
* @param source |
||||
* source UV coordinates |
||||
* @param dest |
||||
* target UV coordinates |
||||
* @param sourceSize |
||||
* the width and height of the source image |
||||
* @param targetSize |
||||
* the width and height of the target image |
||||
* @return affine transform to transform one triangle to another |
||||
*/ |
||||
public AffineTransform createAffineTransform(Vector2f[] source, Vector2f[] dest, int[] sourceSize, int[] targetSize) { |
||||
float x11 = source[0].getX() * sourceSize[0]; |
||||
float x12 = source[0].getY() * sourceSize[1]; |
||||
float x21 = source[1].getX() * sourceSize[0]; |
||||
float x22 = source[1].getY() * sourceSize[1]; |
||||
float x31 = source[2].getX() * sourceSize[0]; |
||||
float x32 = source[2].getY() * sourceSize[1]; |
||||
float y11 = dest[0].getX() * targetSize[0]; |
||||
float y12 = dest[0].getY() * targetSize[1]; |
||||
float y21 = dest[1].getX() * targetSize[0]; |
||||
float y22 = dest[1].getY() * targetSize[1]; |
||||
float y31 = dest[2].getX() * targetSize[0]; |
||||
float y32 = dest[2].getY() * targetSize[1]; |
||||
|
||||
float a1 = ((y11 - y21) * (x12 - x32) - (y11 - y31) * (x12 - x22)) / ((x11 - x21) * (x12 - x32) - (x11 - x31) * (x12 - x22)); |
||||
float a2 = ((y11 - y21) * (x11 - x31) - (y11 - y31) * (x11 - x21)) / ((x12 - x22) * (x11 - x31) - (x12 - x32) * (x11 - x21)); |
||||
float a3 = y11 - a1 * x11 - a2 * x12; |
||||
float a4 = ((y12 - y22) * (x12 - x32) - (y12 - y32) * (x12 - x22)) / ((x11 - x21) * (x12 - x32) - (x11 - x31) * (x12 - x22)); |
||||
float a5 = ((y12 - y22) * (x11 - x31) - (y12 - y32) * (x11 - x21)) / ((x12 - x22) * (x11 - x31) - (x12 - x32) * (x11 - x21)); |
||||
float a6 = y12 - a4 * x11 - a5 * x12; |
||||
return new AffineTransform(a1, a4, a2, a5, a3, a6); |
||||
} |
||||
|
||||
/** |
||||
* This method returns the proper pixel position on the image. |
||||
* |
||||
* @param pos |
||||
* the relative position (value of range [0, 1] (both inclusive)) |
||||
* @param size |
||||
* the size of the line the pixel lies on (width, height or |
||||
* depth) |
||||
* @return the integer index of the pixel on the line of the specified width |
||||
*/ |
||||
public int getPixelPosition(float pos, int size) { |
||||
float pixelWidth = 1 / (float) size; |
||||
pos *= size; |
||||
int result = (int) pos; |
||||
// here is where we repair floating point operations errors :)
|
||||
if (Math.abs(result - pos) > pixelWidth) { |
||||
++result; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method returns subimage of the give image. The subimage is |
||||
* constrained by the rectangle coordinates. The source image is unchanged. |
||||
* |
||||
* @param image |
||||
* the image to be subimaged |
||||
* @param minX |
||||
* minimum X position |
||||
* @param minY |
||||
* minimum Y position |
||||
* @param maxX |
||||
* maximum X position |
||||
* @param maxY |
||||
* maximum Y position |
||||
* @return a part of the given image |
||||
*/ |
||||
public Image getSubimage(Image image, int minX, int minY, int maxX, int maxY) { |
||||
if (minY > maxY) { |
||||
throw new IllegalArgumentException("Minimum Y value is higher than maximum Y value!"); |
||||
} |
||||
if (minX > maxX) { |
||||
throw new IllegalArgumentException("Minimum Y value is higher than maximum Y value!"); |
||||
} |
||||
if (image.getData().size() > 1) { |
||||
throw new IllegalArgumentException("Only flat images are allowed for subimage operation!"); |
||||
} |
||||
if (image.getMipMapSizes() != null) { |
||||
LOGGER.warning("Subimaging image with mipmaps is not yet supported!"); |
||||
} |
||||
|
||||
int width = maxX - minX; |
||||
int height = maxY - minY; |
||||
ByteBuffer data = BufferUtils.createByteBuffer(width * height * (image.getFormat().getBitsPerPixel() >> 3)); |
||||
|
||||
Image result = new Image(image.getFormat(), width, height, data, ColorSpace.sRGB); |
||||
PixelInputOutput pixelIO = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
|
||||
for (int x = minX; x < maxX; ++x) { |
||||
for (int y = minY; y < maxY; ++y) { |
||||
pixelIO.read(image, 0, pixel, x, y); |
||||
pixelIO.write(result, 0, pixel, x - minX, y - minY); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* This method applies the colorband and color factors to image type |
||||
* textures. If there is no colorband defined for the texture or the color |
||||
* factors are all equal to 1.0f then no changes are made. |
||||
* |
||||
* @param tex |
||||
* the texture structure |
||||
* @param image |
||||
* the image that will be altered if necessary |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
private void applyColorbandAndColorFactors(Structure tex, Image image, BlenderContext blenderContext) { |
||||
float rfac = ((Number) tex.getFieldValue("rfac")).floatValue(); |
||||
float gfac = ((Number) tex.getFieldValue("gfac")).floatValue(); |
||||
float bfac = ((Number) tex.getFieldValue("bfac")).floatValue(); |
||||
float[][] colorBand = new ColorBand(tex, blenderContext).computeValues(); |
||||
int depth = image.getDepth() == 0 ? 1 : image.getDepth(); |
||||
|
||||
if (colorBand != null) { |
||||
TexturePixel pixel = new TexturePixel(); |
||||
PixelInputOutput imageIO = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) { |
||||
for (int x = 0; x < image.getWidth(); ++x) { |
||||
for (int y = 0; y < image.getHeight(); ++y) { |
||||
imageIO.read(image, layerIndex, pixel, x, y); |
||||
|
||||
int colorbandIndex = (int) (pixel.alpha * 1000.0f); |
||||
pixel.red = colorBand[colorbandIndex][0] * rfac; |
||||
pixel.green = colorBand[colorbandIndex][1] * gfac; |
||||
pixel.blue = colorBand[colorbandIndex][2] * bfac; |
||||
pixel.alpha = colorBand[colorbandIndex][3]; |
||||
|
||||
imageIO.write(image, layerIndex, pixel, x, y); |
||||
} |
||||
} |
||||
} |
||||
} else if (rfac != 1.0f || gfac != 1.0f || bfac != 1.0f) { |
||||
TexturePixel pixel = new TexturePixel(); |
||||
PixelInputOutput imageIO = PixelIOFactory.getPixelIO(image.getFormat()); |
||||
for (int layerIndex = 0; layerIndex < depth; ++layerIndex) { |
||||
for (int x = 0; x < image.getWidth(); ++x) { |
||||
for (int y = 0; y < image.getHeight(); ++y) { |
||||
imageIO.read(image, layerIndex, pixel, x, y); |
||||
|
||||
pixel.red *= rfac; |
||||
pixel.green *= gfac; |
||||
pixel.blue *= bfac; |
||||
|
||||
imageIO.write(image, layerIndex, pixel, x, y); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method loads the texture from outside the blend file using the |
||||
* AssetManager that the blend file was loaded with. It returns a texture |
||||
* with a full assetKey that references the original texture so it later |
||||
* doesn't need to be packed when the model data is serialized. It searches |
||||
* the AssetManager for the full path if the model file is a relative path |
||||
* and will attempt to truncate the path if it is an absolute file path |
||||
* until the path can be found in the AssetManager. If the texture can not |
||||
* be found, it will issue a load attempt for the initial path anyway so the |
||||
* failed load can be reported by the AssetManagers callback methods for |
||||
* failed assets. |
||||
* |
||||
* @param name |
||||
* the path to the image |
||||
* @param imaflag |
||||
* the image flag |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return the loaded image or null if the image cannot be found |
||||
*/ |
||||
protected Texture loadImageFromFile(String name, int imaflag, BlenderContext blenderContext) { |
||||
if (!name.contains(".")) { |
||||
return null; // no extension means not a valid image
|
||||
} |
||||
|
||||
// decide if the mipmaps will be generated
|
||||
boolean generateMipmaps = false; |
||||
switch (blenderContext.getBlenderKey().getMipmapGenerationMethod()) { |
||||
case ALWAYS_GENERATE: |
||||
generateMipmaps = true; |
||||
break; |
||||
case NEVER_GENERATE: |
||||
break; |
||||
case GENERATE_WHEN_NEEDED: |
||||
generateMipmaps = (imaflag & 0x04) != 0; |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unknown mipmap generation method: " + blenderContext.getBlenderKey().getMipmapGenerationMethod()); |
||||
} |
||||
|
||||
AssetManager assetManager = blenderContext.getAssetManager(); |
||||
name = name.replace('\\', '/'); |
||||
Texture result = null; |
||||
|
||||
if (name.startsWith("//")) { |
||||
// This is a relative path, so try to find it relative to the .blend file
|
||||
String relativePath = name.substring(2); |
||||
// Augument the path with blender key path
|
||||
BlenderKey blenderKey = blenderContext.getBlenderKey(); |
||||
int idx = blenderKey.getName().lastIndexOf('/'); |
||||
String blenderAssetFolder = blenderKey.getName().substring(0, idx != -1 ? idx : 0); |
||||
String absoluteName = blenderAssetFolder + '/' + relativePath; |
||||
// Directly try to load texture so AssetManager can report missing textures
|
||||
try { |
||||
TextureKey key = new TextureKey(absoluteName); |
||||
key.setFlipY(true); |
||||
key.setGenerateMips(generateMipmaps); |
||||
result = assetManager.loadTexture(key); |
||||
result.setKey(key); |
||||
} catch (AssetNotFoundException | AssetLoadException e) { |
||||
LOGGER.fine(e.getLocalizedMessage()); |
||||
} |
||||
} else { |
||||
// This is a full path, try to truncate it until the file can be found
|
||||
// this works as the assetManager root is most probably a part of the
|
||||
// image path. E.g. AssetManager has a locator at c:/Files/ and the
|
||||
// texture path is c:/Files/Textures/Models/Image.jpg.
|
||||
// For this we create a list with every possible full path name from
|
||||
// the asset name to the root. Image.jpg, Models/Image.jpg,
|
||||
// Textures/Models/Image.jpg (bingo) etc.
|
||||
List<String> assetNames = new ArrayList<String>(); |
||||
String[] paths = name.split("\\/"); |
||||
StringBuilder sb = new StringBuilder(paths[paths.length - 1]);// the asset name
|
||||
assetNames.add(paths[paths.length - 1]); |
||||
|
||||
for (int i = paths.length - 2; i >= 0; --i) { |
||||
sb.insert(0, '/'); |
||||
sb.insert(0, paths[i]); |
||||
assetNames.add(0, sb.toString()); |
||||
} |
||||
// Now try to locate the asset
|
||||
for (String assetName : assetNames) { |
||||
try { |
||||
TextureKey key = new TextureKey(assetName); |
||||
key.setFlipY(true); |
||||
key.setGenerateMips(generateMipmaps); |
||||
AssetInfo info = assetManager.locateAsset(key); |
||||
if (info != null) { |
||||
Texture texture = assetManager.loadTexture(key); |
||||
result = texture; |
||||
// Set key explicitly here if other ways fail
|
||||
texture.setKey(key); |
||||
// If texture is found return it;
|
||||
return result; |
||||
} |
||||
} catch (AssetNotFoundException | AssetLoadException e) { |
||||
LOGGER.fine(e.getLocalizedMessage()); |
||||
} |
||||
} |
||||
// The asset was not found in the loop above, call loadTexture with
|
||||
// the original path once anyway so that the AssetManager can report
|
||||
// the missing asset to subsystems.
|
||||
try { |
||||
TextureKey key = new TextureKey(name); |
||||
assetManager.loadTexture(key); |
||||
} catch (AssetNotFoundException | AssetLoadException e) { |
||||
LOGGER.fine(e.getLocalizedMessage()); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Reads the texture data from the given material or sky structure. |
||||
* @param structure |
||||
* the structure of material or sky |
||||
* @param diffuseColorArray |
||||
* array of diffuse colors |
||||
* @param skyTexture |
||||
* indicates it we're going to read sky texture or not |
||||
* @return a list of combined textures |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when problems with reading the blend file occur |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public List<CombinedTexture> readTextureData(Structure structure, float[] diffuseColorArray, boolean skyTexture) throws BlenderFileException { |
||||
DynamicArray<Pointer> mtexsArray = (DynamicArray<Pointer>) structure.getFieldValue("mtex"); |
||||
int separatedTextures = skyTexture ? 0 : ((Number) structure.getFieldValue("septex")).intValue(); |
||||
List<TextureData> texturesList = new ArrayList<TextureData>(); |
||||
for (int i = 0; i < mtexsArray.getTotalSize(); ++i) { |
||||
Pointer p = mtexsArray.get(i); |
||||
if (p.isNotNull() && (separatedTextures & 1 << i) == 0) { |
||||
TextureData textureData = new TextureData(); |
||||
textureData.mtex = p.fetchData().get(0); |
||||
textureData.uvCoordinatesType = skyTexture ? UVCoordinatesType.TEXCO_ORCO.blenderValue : ((Number) textureData.mtex.getFieldValue("texco")).intValue(); |
||||
textureData.projectionType = ((Number) textureData.mtex.getFieldValue("mapping")).intValue(); |
||||
textureData.uvCoordinatesName = textureData.mtex.getFieldValue("uvName").toString(); |
||||
if (textureData.uvCoordinatesName != null && textureData.uvCoordinatesName.trim().length() == 0) { |
||||
textureData.uvCoordinatesName = null; |
||||
} |
||||
|
||||
Pointer pTex = (Pointer) textureData.mtex.getFieldValue("tex"); |
||||
if (pTex.isNotNull()) { |
||||
Structure tex = pTex.fetchData().get(0); |
||||
textureData.textureStructure = tex; |
||||
texturesList.add(textureData); |
||||
} |
||||
} |
||||
} |
||||
|
||||
LOGGER.info("Loading model's textures."); |
||||
List<CombinedTexture> loadedTextures = new ArrayList<CombinedTexture>(); |
||||
if (blenderContext.getBlenderKey().isOptimiseTextures()) { |
||||
LOGGER.fine("Optimising the useage of model's textures."); |
||||
Map<Number, List<TextureData>> textureDataMap = this.sortTextures(texturesList); |
||||
for (Entry<Number, List<TextureData>> entry : textureDataMap.entrySet()) { |
||||
if (entry.getValue().size() > 0) { |
||||
CombinedTexture combinedTexture = new CombinedTexture(entry.getKey().intValue(), !skyTexture); |
||||
for (TextureData textureData : entry.getValue()) { |
||||
int texflag = ((Number) textureData.mtex.getFieldValue("texflag")).intValue(); |
||||
boolean negateTexture = (texflag & 0x04) != 0; |
||||
Texture texture = this.getTexture(textureData.textureStructure, textureData.mtex, blenderContext); |
||||
if (texture != null) { |
||||
int blendType = ((Number) textureData.mtex.getFieldValue("blendtype")).intValue(); |
||||
float[] color = new float[] { ((Number) textureData.mtex.getFieldValue("r")).floatValue(), ((Number) textureData.mtex.getFieldValue("g")).floatValue(), ((Number) textureData.mtex.getFieldValue("b")).floatValue() }; |
||||
float colfac = ((Number) textureData.mtex.getFieldValue("colfac")).floatValue(); |
||||
TextureBlender textureBlender = TextureBlenderFactory.createTextureBlender(texture.getImage().getFormat(), texflag, negateTexture, blendType, diffuseColorArray, color, colfac); |
||||
combinedTexture.add(texture, textureBlender, textureData.uvCoordinatesType, textureData.projectionType, textureData.textureStructure, textureData.uvCoordinatesName, blenderContext); |
||||
} |
||||
} |
||||
if (combinedTexture.getTexturesCount() > 0) { |
||||
loadedTextures.add(combinedTexture); |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
LOGGER.fine("No textures optimisation applied."); |
||||
int[] mappings = new int[] { MaterialContext.MTEX_COL, MaterialContext.MTEX_NOR, MaterialContext.MTEX_EMIT, MaterialContext.MTEX_SPEC, MaterialContext.MTEX_ALPHA, MaterialContext.MTEX_AMB }; |
||||
for (TextureData textureData : texturesList) { |
||||
Texture texture = this.getTexture(textureData.textureStructure, textureData.mtex, blenderContext); |
||||
if (texture != null) { |
||||
Number mapto = (Number) textureData.mtex.getFieldValue("mapto"); |
||||
int texflag = ((Number) textureData.mtex.getFieldValue("texflag")).intValue(); |
||||
boolean negateTexture = (texflag & 0x04) != 0; |
||||
|
||||
boolean colorSet = false; |
||||
for (int i = 0; i < mappings.length; ++i) { |
||||
if ((mappings[i] & mapto.intValue()) != 0) { |
||||
if(mappings[i] == MaterialContext.MTEX_COL) { |
||||
colorSet = true; |
||||
} else if(colorSet && mappings[i] == MaterialContext.MTEX_ALPHA) { |
||||
continue; |
||||
} |
||||
|
||||
CombinedTexture combinedTexture = new CombinedTexture(mappings[i], !skyTexture); |
||||
int blendType = ((Number) textureData.mtex.getFieldValue("blendtype")).intValue(); |
||||
float[] color = new float[] { ((Number) textureData.mtex.getFieldValue("r")).floatValue(), ((Number) textureData.mtex.getFieldValue("g")).floatValue(), ((Number) textureData.mtex.getFieldValue("b")).floatValue() }; |
||||
float colfac = ((Number) textureData.mtex.getFieldValue("colfac")).floatValue(); |
||||
TextureBlender textureBlender = TextureBlenderFactory.createTextureBlender(texture.getImage().getFormat(), texflag, negateTexture, blendType, diffuseColorArray, color, colfac); |
||||
combinedTexture.add(texture, textureBlender, textureData.uvCoordinatesType, textureData.projectionType, textureData.textureStructure, textureData.uvCoordinatesName, blenderContext); |
||||
if (combinedTexture.getTexturesCount() > 0) {// the added texture might not have been accepted (if for example loading generated textures is disabled)
|
||||
loadedTextures.add(combinedTexture); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
return loadedTextures; |
||||
} |
||||
|
||||
/** |
||||
* This method sorts the textures by their mapping type. In each group only |
||||
* textures of one type are put (either two- or three-dimensional). |
||||
* |
||||
* @return a map with sorted textures |
||||
*/ |
||||
private Map<Number, List<TextureData>> sortTextures(List<TextureData> textures) { |
||||
int[] mappings = new int[] { MaterialContext.MTEX_COL, MaterialContext.MTEX_NOR, MaterialContext.MTEX_EMIT, MaterialContext.MTEX_SPEC, MaterialContext.MTEX_ALPHA, MaterialContext.MTEX_AMB }; |
||||
Map<Number, List<TextureData>> result = new HashMap<Number, List<TextureData>>(); |
||||
for (TextureData data : textures) { |
||||
Number mapto = (Number) data.mtex.getFieldValue("mapto"); |
||||
|
||||
boolean colorSet = false; |
||||
for (int i = 0; i < mappings.length; ++i) { |
||||
if ((mappings[i] & mapto.intValue()) != 0) { |
||||
if(mappings[i] == MaterialContext.MTEX_COL) { |
||||
colorSet = true; |
||||
} else if(colorSet && mappings[i] == MaterialContext.MTEX_ALPHA) { |
||||
continue; |
||||
} |
||||
|
||||
List<TextureData> datas = result.get(mappings[i]); |
||||
if (datas == null) { |
||||
datas = new ArrayList<TextureData>(); |
||||
result.put(mappings[i], datas); |
||||
} |
||||
datas.add(data); |
||||
} |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private static class TextureData { |
||||
public Structure mtex; |
||||
public Structure textureStructure; |
||||
public int uvCoordinatesType; |
||||
public int projectionType; |
||||
/** The name of the user's UV coordinates that are used for this texture. */ |
||||
public String uvCoordinatesName; |
||||
} |
||||
} |
@ -0,0 +1,392 @@ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import com.jme3.math.ColorRGBA; |
||||
import com.jme3.math.FastMath; |
||||
|
||||
/** |
||||
* The class that stores the pixel values of a texture. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class TexturePixel implements Cloneable { |
||||
/** The pixel data. */ |
||||
public float intensity, red, green, blue, alpha; |
||||
|
||||
/** |
||||
* Copies the values from the given pixel. |
||||
* |
||||
* @param pixel |
||||
* the pixel that we read from |
||||
*/ |
||||
public void fromPixel(TexturePixel pixel) { |
||||
this.intensity = pixel.intensity; |
||||
this.red = pixel.red; |
||||
this.green = pixel.green; |
||||
this.blue = pixel.blue; |
||||
this.alpha = pixel.alpha; |
||||
} |
||||
|
||||
/** |
||||
* Copies the values from the given color. |
||||
* |
||||
* @param colorRGBA |
||||
* the color that we read from |
||||
*/ |
||||
public void fromColor(ColorRGBA colorRGBA) { |
||||
this.red = colorRGBA.r; |
||||
this.green = colorRGBA.g; |
||||
this.blue = colorRGBA.b; |
||||
this.alpha = colorRGBA.a; |
||||
} |
||||
|
||||
/** |
||||
* Copies the values from the given values. |
||||
* |
||||
* @param a |
||||
* the alpha value |
||||
* @param r |
||||
* the red value |
||||
* @param g |
||||
* the green value |
||||
* @param b |
||||
* the blue value |
||||
*/ |
||||
public void fromARGB(float a, float r, float g, float b) { |
||||
this.alpha = a; |
||||
this.red = r; |
||||
this.green = g; |
||||
this.blue = b; |
||||
} |
||||
|
||||
/** |
||||
* Copies the values from the given values. |
||||
* |
||||
* @param a |
||||
* the alpha value |
||||
* @param r |
||||
* the red value |
||||
* @param g |
||||
* the green value |
||||
* @param b |
||||
* the blue value |
||||
*/ |
||||
public void fromARGB8(byte a, byte r, byte g, byte b) { |
||||
this.alpha = a >= 0 ? a / 255.0f : 1.0f - ~a / 255.0f; |
||||
this.red = r >= 0 ? r / 255.0f : 1.0f - ~r / 255.0f; |
||||
this.green = g >= 0 ? g / 255.0f : 1.0f - ~g / 255.0f; |
||||
this.blue = b >= 0 ? b / 255.0f : 1.0f - ~b / 255.0f; |
||||
} |
||||
|
||||
/** |
||||
* Copies the values from the given values. |
||||
* |
||||
* @param a |
||||
* the alpha value |
||||
* @param r |
||||
* the red value |
||||
* @param g |
||||
* the green value |
||||
* @param b |
||||
* the blue value |
||||
*/ |
||||
public void fromARGB16(short a, short r, short g, short b) { |
||||
this.alpha = a >= 0 ? a / 65535.0f : 1.0f - ~a / 65535.0f; |
||||
this.red = r >= 0 ? r / 65535.0f : 1.0f - ~r / 65535.0f; |
||||
this.green = g >= 0 ? g / 65535.0f : 1.0f - ~g / 65535.0f; |
||||
this.blue = b >= 0 ? b / 65535.0f : 1.0f - ~b / 65535.0f; |
||||
} |
||||
|
||||
/** |
||||
* Copies the intensity from the given value. |
||||
* |
||||
* @param intensity |
||||
* the intensity value |
||||
*/ |
||||
public void fromIntensity(byte intensity) { |
||||
this.intensity = intensity >= 0 ? intensity / 255.0f : 1.0f - ~intensity / 255.0f; |
||||
} |
||||
|
||||
/** |
||||
* Copies the intensity from the given value. |
||||
* |
||||
* @param intensity |
||||
* the intensity value |
||||
*/ |
||||
public void fromIntensity(short intensity) { |
||||
this.intensity = intensity >= 0 ? intensity / 65535.0f : 1.0f - ~intensity / 65535.0f; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the alpha value (converts it to float number from range |
||||
* [0, 1]). |
||||
* |
||||
* @param alpha |
||||
* the alpha value |
||||
*/ |
||||
public void setAlpha(byte alpha) { |
||||
this.alpha = alpha >= 0 ? alpha / 255.0f : 1.0f - ~alpha / 255.0f; |
||||
} |
||||
|
||||
/** |
||||
* This method sets the alpha value (converts it to float number from range |
||||
* [0, 1]). |
||||
* |
||||
* @param alpha |
||||
* the alpha value |
||||
*/ |
||||
public void setAlpha(short alpha) { |
||||
this.alpha = alpha >= 0 ? alpha / 65535.0f : 1.0f - ~alpha / 65535.0f; |
||||
} |
||||
|
||||
/** |
||||
* Copies the values from the given integer that stores the ARGB8 data. |
||||
* |
||||
* @param argb8 |
||||
* the data stored in an integer |
||||
*/ |
||||
public void fromARGB8(int argb8) { |
||||
byte pixelValue = (byte) ((argb8 & 0xFF000000) >> 24); |
||||
this.alpha = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f; |
||||
pixelValue = (byte) ((argb8 & 0xFF0000) >> 16); |
||||
this.red = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f; |
||||
pixelValue = (byte) ((argb8 & 0xFF00) >> 8); |
||||
this.green = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f; |
||||
pixelValue = (byte) (argb8 & 0xFF); |
||||
this.blue = pixelValue >= 0 ? pixelValue / 255.0f : 1.0f - ~pixelValue / 255.0f; |
||||
} |
||||
|
||||
/** |
||||
* Stores RGBA values in the given array. |
||||
* |
||||
* @param result |
||||
* the array to store values |
||||
*/ |
||||
public void toRGBA(float[] result) { |
||||
result[0] = this.red; |
||||
result[1] = this.green; |
||||
result[2] = this.blue; |
||||
result[3] = this.alpha; |
||||
} |
||||
|
||||
/** |
||||
* Stores the data in the given table. |
||||
* |
||||
* @param result |
||||
* the result table |
||||
*/ |
||||
public void toRGBA8(byte[] result) { |
||||
result[0] = (byte) (this.red * 255.0f); |
||||
result[1] = (byte) (this.green * 255.0f); |
||||
result[2] = (byte) (this.blue * 255.0f); |
||||
result[3] = (byte) (this.alpha * 255.0f); |
||||
} |
||||
|
||||
/** |
||||
* Stores the pixel values in the integer. |
||||
* |
||||
* @return the integer that stores the pixel values |
||||
*/ |
||||
public int toARGB8() { |
||||
int result = 0; |
||||
int b = (int) (this.alpha * 255.0f); |
||||
result |= b << 24; |
||||
b = (int) (this.red * 255.0f); |
||||
result |= b << 16; |
||||
b = (int) (this.green * 255.0f); |
||||
result |= b << 8; |
||||
b = (int) (this.blue * 255.0f); |
||||
result |= b; |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* @return the intensity of the pixel |
||||
*/ |
||||
public byte getInt() { |
||||
return (byte) (this.intensity * 255.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the alpha value of the pixel |
||||
*/ |
||||
public byte getA8() { |
||||
return (byte) (this.alpha * 255.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the alpha red of the pixel |
||||
*/ |
||||
public byte getR8() { |
||||
return (byte) (this.red * 255.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the green value of the pixel |
||||
*/ |
||||
public byte getG8() { |
||||
return (byte) (this.green * 255.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the blue value of the pixel |
||||
*/ |
||||
public byte getB8() { |
||||
return (byte) (this.blue * 255.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the alpha value of the pixel |
||||
*/ |
||||
public short getA16() { |
||||
return (byte) (this.alpha * 65535.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the alpha red of the pixel |
||||
*/ |
||||
public short getR16() { |
||||
return (byte) (this.red * 65535.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the green value of the pixel |
||||
*/ |
||||
public short getG16() { |
||||
return (byte) (this.green * 65535.0f); |
||||
} |
||||
|
||||
/** |
||||
* @return the blue value of the pixel |
||||
*/ |
||||
public short getB16() { |
||||
return (byte) (this.blue * 65535.0f); |
||||
} |
||||
|
||||
/** |
||||
* Merges two pixels (adds the values of each color). |
||||
* |
||||
* @param pixel |
||||
* the pixel we merge with |
||||
*/ |
||||
public void merge(TexturePixel pixel) { |
||||
float oneMinusAlpha = 1 - pixel.alpha; |
||||
this.red = oneMinusAlpha * this.red + pixel.alpha * pixel.red; |
||||
this.green = oneMinusAlpha * this.green + pixel.alpha * pixel.green; |
||||
this.blue = oneMinusAlpha * this.blue + pixel.alpha * pixel.blue; |
||||
this.alpha = (this.alpha + pixel.alpha) * 0.5f; |
||||
} |
||||
|
||||
/** |
||||
* Mixes two pixels. |
||||
* |
||||
* @param pixel |
||||
* the pixel we mix with |
||||
*/ |
||||
public void mix(TexturePixel pixel) { |
||||
this.red = 0.5f * (this.red + pixel.red); |
||||
this.green = 0.5f * (this.green + pixel.green); |
||||
this.blue = 0.5f * (this.blue + pixel.blue); |
||||
this.alpha = 0.5f * (this.alpha + pixel.alpha); |
||||
this.intensity = 0.5f * (this.intensity + pixel.intensity); |
||||
} |
||||
|
||||
/** |
||||
* This method negates the colors. |
||||
*/ |
||||
public void negate() { |
||||
this.red = 1.0f - this.red; |
||||
this.green = 1.0f - this.green; |
||||
this.blue = 1.0f - this.blue; |
||||
this.alpha = 1.0f - this.alpha; |
||||
} |
||||
|
||||
/** |
||||
* This method clears the pixel values. |
||||
*/ |
||||
public void clear() { |
||||
this.intensity = this.blue = this.red = this.green = this.alpha = 0.0f; |
||||
} |
||||
|
||||
/** |
||||
* This method adds the calues of the given pixel to the current pixel. |
||||
* |
||||
* @param pixel |
||||
* the pixel we add |
||||
*/ |
||||
public void add(TexturePixel pixel) { |
||||
this.red += pixel.red; |
||||
this.green += pixel.green; |
||||
this.blue += pixel.blue; |
||||
this.alpha += pixel.alpha; |
||||
this.intensity += pixel.intensity; |
||||
} |
||||
|
||||
/** |
||||
* This method adds the calues of the given pixel to the current pixel. |
||||
* |
||||
* @param pixel |
||||
* the pixel we add |
||||
*/ |
||||
public void add(ColorRGBA pixel) { |
||||
this.red += pixel.r; |
||||
this.green += pixel.g; |
||||
this.blue += pixel.b; |
||||
this.alpha += pixel.a; |
||||
} |
||||
|
||||
/** |
||||
* This method multiplies the values of the given pixel by the given value. |
||||
* |
||||
* @param value |
||||
* multiplication factor |
||||
*/ |
||||
public void mult(float value) { |
||||
this.red *= value; |
||||
this.green *= value; |
||||
this.blue *= value; |
||||
this.alpha *= value; |
||||
this.intensity *= value; |
||||
} |
||||
|
||||
/** |
||||
* This method divides the values of the given pixel by the given value. |
||||
* ATTENTION! Beware of the zero value. This will cause you NaN's in the |
||||
* pixel values. |
||||
* |
||||
* @param value |
||||
* division factor |
||||
*/ |
||||
public void divide(float value) { |
||||
this.red /= value; |
||||
this.green /= value; |
||||
this.blue /= value; |
||||
this.alpha /= value; |
||||
this.intensity /= value; |
||||
} |
||||
|
||||
/** |
||||
* This method clamps the pixel values to the given borders. |
||||
* |
||||
* @param min |
||||
* the minimum value |
||||
* @param max |
||||
* the maximum value |
||||
*/ |
||||
public void clamp(float min, float max) { |
||||
this.red = FastMath.clamp(this.red, min, max); |
||||
this.green = FastMath.clamp(this.green, min, max); |
||||
this.blue = FastMath.clamp(this.blue, min, max); |
||||
this.alpha = FastMath.clamp(this.alpha, min, max); |
||||
this.intensity = FastMath.clamp(this.intensity, min, max); |
||||
} |
||||
|
||||
@Override |
||||
public Object clone() throws CloneNotSupportedException { |
||||
return super.clone(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "[" + red + ", " + green + ", " + blue + ", " + alpha + " {" + intensity + "}]"; |
||||
} |
||||
} |
@ -0,0 +1,662 @@ |
||||
package com.jme3.scene.plugins.blender.textures; |
||||
|
||||
import java.awt.Graphics2D; |
||||
import java.awt.RenderingHints; |
||||
import java.awt.geom.AffineTransform; |
||||
import java.awt.image.BufferedImage; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.Set; |
||||
import java.util.TreeSet; |
||||
|
||||
import jme3tools.converters.ImageToAwt; |
||||
|
||||
import com.jme3.bounding.BoundingBox; |
||||
import com.jme3.math.FastMath; |
||||
import com.jme3.math.Vector2f; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.plugins.blender.BlenderContext; |
||||
import com.jme3.scene.plugins.blender.textures.blending.TextureBlender; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelIOFactory; |
||||
import com.jme3.scene.plugins.blender.textures.io.PixelInputOutput; |
||||
import com.jme3.texture.Image; |
||||
import com.jme3.texture.Image.Format; |
||||
import com.jme3.texture.Texture; |
||||
import com.jme3.texture.Texture2D; |
||||
import com.jme3.texture.image.ColorSpace; |
||||
import com.jme3.util.BufferUtils; |
||||
|
||||
/** |
||||
* This texture holds a set of images for each face in the specified mesh. It |
||||
* helps to flatten 3D texture, merge 3D and 2D textures and merge 2D textures |
||||
* with different UV coordinates. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */class TriangulatedTexture extends Texture2D { |
||||
/** The result image format. */ |
||||
private Format format; |
||||
/** The collection of images for each face. */ |
||||
private Collection<TriangleTextureElement> faceTextures; |
||||
/** |
||||
* The maximum texture size (width/height). This is taken from the blender |
||||
* key. |
||||
*/ |
||||
private int maxTextureSize; |
||||
/** A variable that can prevent removing identical textures. */ |
||||
private boolean keepIdenticalTextures = false; |
||||
/** The result texture. */ |
||||
private Texture2D resultTexture; |
||||
/** The result texture's UV coordinates. */ |
||||
private List<Vector2f> resultUVS; |
||||
|
||||
/** |
||||
* This method triangulates the given flat texture. The given texture is not |
||||
* changed. |
||||
* |
||||
* @param texture2d |
||||
* the texture to be triangulated |
||||
* @param uvs |
||||
* the UV coordinates for each face |
||||
*/ |
||||
public TriangulatedTexture(Texture2D texture2d, List<Vector2f> uvs, BlenderContext blenderContext) { |
||||
maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize(); |
||||
faceTextures = new TreeSet<TriangleTextureElement>(new Comparator<TriangleTextureElement>() { |
||||
@Override |
||||
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) { |
||||
return o1.faceIndex - o2.faceIndex; |
||||
} |
||||
}); |
||||
int facesCount = uvs.size() / 3; |
||||
for (int i = 0; i < facesCount; ++i) { |
||||
faceTextures.add(new TriangleTextureElement(i, texture2d.getImage(), uvs, true, blenderContext)); |
||||
} |
||||
format = texture2d.getImage().getFormat(); |
||||
} |
||||
|
||||
/** |
||||
* Constructor that simply stores precalculated images. |
||||
* |
||||
* @param faceTextures |
||||
* a collection of images for the mesh's faces |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public TriangulatedTexture(Collection<TriangleTextureElement> faceTextures, BlenderContext blenderContext) { |
||||
maxTextureSize = blenderContext.getBlenderKey().getMaxTextureSize(); |
||||
this.faceTextures = faceTextures; |
||||
for (TriangleTextureElement faceTextureElement : faceTextures) { |
||||
if (format == null) { |
||||
format = faceTextureElement.image.getFormat(); |
||||
} else if (format != faceTextureElement.image.getFormat()) { |
||||
throw new IllegalArgumentException("Face texture element images MUST have the same image format!"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method blends the each image using the given blender and taking base |
||||
* texture into consideration. |
||||
* |
||||
* @param textureBlender |
||||
* the texture blender that holds the blending definition |
||||
* @param baseTexture |
||||
* the texture that is 'below' the current texture (can be null) |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public void blend(TextureBlender textureBlender, TriangulatedTexture baseTexture, BlenderContext blenderContext) { |
||||
Format newFormat = null; |
||||
for (TriangleTextureElement triangleTextureElement : faceTextures) { |
||||
Image baseImage = baseTexture == null ? null : baseTexture.getFaceTextureElement(triangleTextureElement.faceIndex).image; |
||||
triangleTextureElement.image = textureBlender.blend(triangleTextureElement.image, baseImage, blenderContext); |
||||
if (newFormat == null) { |
||||
newFormat = triangleTextureElement.image.getFormat(); |
||||
} else if (newFormat != triangleTextureElement.image.getFormat()) { |
||||
throw new IllegalArgumentException("Face texture element images MUST have the same image format!"); |
||||
} |
||||
} |
||||
format = newFormat; |
||||
} |
||||
|
||||
/** |
||||
* This method alters the images to fit them into UV coordinates of the |
||||
* given target texture. |
||||
* |
||||
* @param targetTexture |
||||
* the texture to whose UV coordinates we fit current images |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public void castToUVS(TriangulatedTexture targetTexture, BlenderContext blenderContext) { |
||||
int[] sourceSize = new int[2], targetSize = new int[2]; |
||||
ImageLoader imageLoader = new ImageLoader(); |
||||
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class); |
||||
for (TriangleTextureElement entry : faceTextures) { |
||||
TriangleTextureElement targetFaceTextureElement = targetTexture.getFaceTextureElement(entry.faceIndex); |
||||
Vector2f[] dest = targetFaceTextureElement.uv; |
||||
|
||||
// get the sizes of the source and target images
|
||||
sourceSize[0] = entry.image.getWidth(); |
||||
sourceSize[1] = entry.image.getHeight(); |
||||
targetSize[0] = targetFaceTextureElement.image.getWidth(); |
||||
targetSize[1] = targetFaceTextureElement.image.getHeight(); |
||||
|
||||
// create triangle transformation
|
||||
AffineTransform affineTransform = textureHelper.createAffineTransform(entry.uv, dest, sourceSize, targetSize); |
||||
|
||||
// compute the result texture
|
||||
BufferedImage sourceImage = ImageToAwt.convert(entry.image, false, true, 0); |
||||
|
||||
BufferedImage targetImage = new BufferedImage(targetSize[0], targetSize[1], sourceImage.getType()); |
||||
Graphics2D g = targetImage.createGraphics(); |
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
||||
g.drawImage(sourceImage, affineTransform, null); |
||||
g.dispose(); |
||||
|
||||
Image output = imageLoader.load(targetImage, false); |
||||
entry.image = output; |
||||
entry.uv[0].set(dest[0]); |
||||
entry.uv[1].set(dest[1]); |
||||
entry.uv[2].set(dest[2]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns the flat texture. It is calculated if required or if |
||||
* it was not created before. Images that are identical are discarded to |
||||
* reduce the texture size. |
||||
* |
||||
* @param rebuild |
||||
* a variable that forces texture recomputation (even if it was |
||||
* computed vefore) |
||||
* @return flat result texture (all images merged into one) |
||||
*/ |
||||
public Texture2D getResultTexture(boolean rebuild) { |
||||
if (resultTexture == null || rebuild) { |
||||
// sorting the parts by their height (from highest to the lowest)
|
||||
List<TriangleTextureElement> list = new ArrayList<TriangleTextureElement>(faceTextures); |
||||
Collections.sort(list, new Comparator<TriangleTextureElement>() { |
||||
@Override |
||||
public int compare(TriangleTextureElement o1, TriangleTextureElement o2) { |
||||
return o2.image.getHeight() - o1.image.getHeight(); |
||||
} |
||||
}); |
||||
|
||||
// arraging the images on the resulting image (calculating the result image width and height)
|
||||
Set<Integer> duplicatedFaceIndexes = new HashSet<Integer>(); |
||||
int resultImageHeight = list.get(0).image.getHeight(); |
||||
int resultImageWidth = 0; |
||||
int currentXPos = 0, currentYPos = 0; |
||||
Map<TriangleTextureElement, Integer[]> imageLayoutData = new HashMap<TriangleTextureElement, Integer[]>(list.size()); |
||||
while (list.size() > 0) { |
||||
TriangleTextureElement currentElement = list.remove(0); |
||||
if (currentXPos + currentElement.image.getWidth() > maxTextureSize) { |
||||
currentXPos = 0; |
||||
currentYPos = resultImageHeight; |
||||
resultImageHeight += currentElement.image.getHeight(); |
||||
} |
||||
Integer[] currentPositions = new Integer[] { currentXPos, currentYPos }; |
||||
imageLayoutData.put(currentElement, currentPositions); |
||||
|
||||
if (keepIdenticalTextures) {// removing identical images
|
||||
for (int i = 0; i < list.size(); ++i) { |
||||
if (currentElement.image.equals(list.get(i).image)) { |
||||
duplicatedFaceIndexes.add(list.get(i).faceIndex); |
||||
imageLayoutData.put(list.remove(i--), currentPositions); |
||||
} |
||||
} |
||||
} |
||||
|
||||
currentXPos += currentElement.image.getWidth(); |
||||
resultImageWidth = Math.max(resultImageWidth, currentXPos); |
||||
// currentYPos += currentElement.image.getHeight();
|
||||
|
||||
// TODO: implement that to compact the result image
|
||||
// try to add smaller images below the current one
|
||||
// int remainingHeight = resultImageHeight -
|
||||
// currentElement.image.getHeight();
|
||||
// while(remainingHeight > 0) {
|
||||
// for(int i=list.size() - 1;i>=0;--i) {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
} |
||||
|
||||
// computing the result UV coordinates
|
||||
resultUVS = new ArrayList<Vector2f>(imageLayoutData.size() * 3); |
||||
for (int i = 0; i < imageLayoutData.size() * 3; ++i) { |
||||
resultUVS.add(null); |
||||
} |
||||
Vector2f[] uvs = new Vector2f[3]; |
||||
for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) { |
||||
Integer[] position = entry.getValue(); |
||||
entry.getKey().computeFinalUVCoordinates(resultImageWidth, resultImageHeight, position[0], position[1], uvs); |
||||
resultUVS.set(entry.getKey().faceIndex * 3, uvs[0]); |
||||
resultUVS.set(entry.getKey().faceIndex * 3 + 1, uvs[1]); |
||||
resultUVS.set(entry.getKey().faceIndex * 3 + 2, uvs[2]); |
||||
} |
||||
|
||||
Image resultImage = new Image(format, resultImageWidth, resultImageHeight, BufferUtils.createByteBuffer(resultImageWidth * resultImageHeight * (format.getBitsPerPixel() >> 3)), ColorSpace.Linear); |
||||
resultTexture = new Texture2D(resultImage); |
||||
for (Entry<TriangleTextureElement, Integer[]> entry : imageLayoutData.entrySet()) { |
||||
if (!duplicatedFaceIndexes.contains(entry.getKey().faceIndex)) { |
||||
this.draw(resultImage, entry.getKey().image, entry.getValue()[0], entry.getValue()[1]); |
||||
} |
||||
} |
||||
|
||||
// setting additional data
|
||||
resultTexture.setWrap(WrapAxis.S, this.getWrap(WrapAxis.S)); |
||||
resultTexture.setWrap(WrapAxis.T, this.getWrap(WrapAxis.T)); |
||||
resultTexture.setMagFilter(this.getMagFilter()); |
||||
resultTexture.setMinFilter(this.getMinFilter()); |
||||
} |
||||
return resultTexture; |
||||
} |
||||
|
||||
/** |
||||
* @return the result flat texture |
||||
*/ |
||||
public Texture2D getResultTexture() { |
||||
return this.getResultTexture(false); |
||||
} |
||||
|
||||
/** |
||||
* @return the result texture's UV coordinates |
||||
*/ |
||||
public List<Vector2f> getResultUVS() { |
||||
this.getResultTexture();// this is called here to make sure that the result UVS are computed
|
||||
return resultUVS; |
||||
} |
||||
|
||||
/** |
||||
* This method returns a single image element for the given face index. |
||||
* |
||||
* @param faceIndex |
||||
* the face index |
||||
* @return image element for the required face index |
||||
* @throws IllegalStateException |
||||
* this exception is thrown if the current image set does not |
||||
* contain an image for the given face index |
||||
*/ |
||||
public TriangleTextureElement getFaceTextureElement(int faceIndex) { |
||||
for (TriangleTextureElement textureElement : faceTextures) { |
||||
if (textureElement.faceIndex == faceIndex) { |
||||
return textureElement; |
||||
} |
||||
} |
||||
throw new IllegalStateException("No face texture element found for index: " + faceIndex); |
||||
} |
||||
|
||||
/** |
||||
* @return the amount of texture faces |
||||
*/ |
||||
public int getFaceTextureCount() { |
||||
return faceTextures.size(); |
||||
} |
||||
|
||||
/** |
||||
* Tells the object wheather to keep or reduce identical face textures. |
||||
* |
||||
* @param keepIdenticalTextures |
||||
* keeps or discards identical textures |
||||
*/ |
||||
public void setKeepIdenticalTextures(boolean keepIdenticalTextures) { |
||||
this.keepIdenticalTextures = keepIdenticalTextures; |
||||
} |
||||
|
||||
/** |
||||
* This method draws the source image on the target image starting with the |
||||
* specified positions. |
||||
* |
||||
* @param target |
||||
* the target image |
||||
* @param source |
||||
* the source image |
||||
* @param targetXPos |
||||
* start X position on the target image |
||||
* @param targetYPos |
||||
* start Y position on the target image |
||||
*/ |
||||
private void draw(Image target, Image source, int targetXPos, int targetYPos) { |
||||
PixelInputOutput sourceIO = PixelIOFactory.getPixelIO(source.getFormat()); |
||||
PixelInputOutput targetIO = PixelIOFactory.getPixelIO(target.getFormat()); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
|
||||
for (int x = 0; x < source.getWidth(); ++x) { |
||||
for (int y = 0; y < source.getHeight(); ++y) { |
||||
sourceIO.read(source, 0, pixel, x, y); |
||||
targetIO.write(target, 0, pixel, targetXPos + x, targetYPos + y); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A class that represents an image for a single face of the mesh. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
/* package */static class TriangleTextureElement { |
||||
/** The image for the face. */ |
||||
public Image image; |
||||
/** The UV coordinates for the image. */ |
||||
public final Vector2f[] uv; |
||||
/** The index of the face this image refers to. */ |
||||
public final int faceIndex; |
||||
|
||||
/** |
||||
* Constructor that creates the image element from the given texture and |
||||
* UV coordinates (it cuts out the smallest rectasngle possible from the |
||||
* given image that will hold the triangle defined by the given UV |
||||
* coordinates). After the image is cut out the UV coordinates are |
||||
* recalculated to be fit for the new image. |
||||
* |
||||
* @param faceIndex |
||||
* the index of mesh's face this image refers to |
||||
* @param sourceImage |
||||
* the source image |
||||
* @param uvCoordinates |
||||
* the UV coordinates that define the image |
||||
*/ |
||||
public TriangleTextureElement(int faceIndex, Image sourceImage, List<Vector2f> uvCoordinates, boolean wholeUVList, BlenderContext blenderContext) { |
||||
TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class); |
||||
this.faceIndex = faceIndex; |
||||
|
||||
uv = wholeUVList ? new Vector2f[] { uvCoordinates.get(faceIndex * 3).clone(), uvCoordinates.get(faceIndex * 3 + 1).clone(), uvCoordinates.get(faceIndex * 3 + 2).clone() } : new Vector2f[] { uvCoordinates.get(0).clone(), uvCoordinates.get(1).clone(), uvCoordinates.get(2).clone() }; |
||||
|
||||
// be careful here, floating point operations might cause the
|
||||
// texture positions to be inapropriate
|
||||
int[][] texturePosition = new int[3][2]; |
||||
for (int i = 0; i < texturePosition.length; ++i) { |
||||
texturePosition[i][0] = textureHelper.getPixelPosition(uv[i].x, sourceImage.getWidth()); |
||||
texturePosition[i][1] = textureHelper.getPixelPosition(uv[i].y, sourceImage.getHeight()); |
||||
} |
||||
|
||||
// calculating the extent of the texture
|
||||
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE; |
||||
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; |
||||
float minUVX = Float.MAX_VALUE, minUVY = Float.MAX_VALUE; |
||||
float maxUVX = Float.MIN_VALUE, maxUVY = Float.MIN_VALUE; |
||||
|
||||
for (int i = 0; i < texturePosition.length; ++i) { |
||||
minX = Math.min(texturePosition[i][0], minX); |
||||
minY = Math.min(texturePosition[i][1], minY); |
||||
|
||||
maxX = Math.max(texturePosition[i][0], maxX); |
||||
maxY = Math.max(texturePosition[i][1], maxY); |
||||
|
||||
minUVX = Math.min(uv[i].x, minUVX); |
||||
minUVY = Math.min(uv[i].y, minUVY); |
||||
maxUVX = Math.max(uv[i].x, maxUVX); |
||||
maxUVY = Math.max(uv[i].y, maxUVY); |
||||
} |
||||
int width = maxX - minX; |
||||
int height = maxY - minY; |
||||
|
||||
if (width == 0) { |
||||
width = 1; |
||||
} |
||||
if (height == 0) { |
||||
height = 1; |
||||
} |
||||
|
||||
// copy the pixel from the texture to the result image
|
||||
PixelInputOutput pixelReader = PixelIOFactory.getPixelIO(sourceImage.getFormat()); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
ByteBuffer data = BufferUtils.createByteBuffer(width * height * 4); |
||||
for (int y = minY; y < maxY; ++y) { |
||||
for (int x = minX; x < maxX; ++x) { |
||||
int xPos = x >= sourceImage.getWidth() ? x - sourceImage.getWidth() : x; |
||||
int yPos = y >= sourceImage.getHeight() ? y - sourceImage.getHeight() : y; |
||||
pixelReader.read(sourceImage, 0, pixel, xPos, yPos); |
||||
data.put(pixel.getR8()); |
||||
data.put(pixel.getG8()); |
||||
data.put(pixel.getB8()); |
||||
data.put(pixel.getA8()); |
||||
} |
||||
} |
||||
image = new Image(Format.RGBA8, width, height, data, ColorSpace.Linear); |
||||
|
||||
// modify the UV values so that they fit the new image
|
||||
float heightUV = maxUVY - minUVY; |
||||
float widthUV = maxUVX - minUVX; |
||||
for (int i = 0; i < uv.length; ++i) { |
||||
// first translate it to the image borders
|
||||
uv[i].x -= minUVX; |
||||
uv[i].y -= minUVY; |
||||
// then scale so that it fills the whole area
|
||||
uv[i].x /= widthUV; |
||||
uv[i].y /= heightUV; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Constructor that creates an image element from the 3D texture |
||||
* (generated texture). It computes a flat smallest rectangle that can |
||||
* hold a (3D) triangle defined by the given UV coordinates. Then it |
||||
* defines the image pixels for points in 3D space that define the |
||||
* calculated rectangle. |
||||
* |
||||
* @param faceIndex |
||||
* the face index this image refers to |
||||
* @param boundingBox |
||||
* the bounding box of the mesh |
||||
* @param texture |
||||
* the texture that allows to compute a pixel value in 3D |
||||
* space |
||||
* @param uv |
||||
* the UV coordinates of the mesh |
||||
* @param blenderContext |
||||
* the blender context |
||||
*/ |
||||
public TriangleTextureElement(int faceIndex, BoundingBox boundingBox, GeneratedTexture texture, Vector3f[] uv, int[] uvIndices, BlenderContext blenderContext) { |
||||
this.faceIndex = faceIndex; |
||||
|
||||
// compute the face vertices from the UV coordinates
|
||||
float width = boundingBox.getXExtent() * 2; |
||||
float height = boundingBox.getYExtent() * 2; |
||||
float depth = boundingBox.getZExtent() * 2; |
||||
|
||||
Vector3f min = boundingBox.getMin(null); |
||||
Vector3f v1 = min.add(uv[uvIndices[0]].x * width, uv[uvIndices[0]].y * height, uv[uvIndices[0]].z * depth); |
||||
Vector3f v2 = min.add(uv[uvIndices[1]].x * width, uv[uvIndices[1]].y * height, uv[uvIndices[1]].z * depth); |
||||
Vector3f v3 = min.add(uv[uvIndices[2]].x * width, uv[uvIndices[2]].y * height, uv[uvIndices[2]].z * depth); |
||||
|
||||
// get the rectangle envelope for the triangle
|
||||
RectangleEnvelope envelope = this.getTriangleEnvelope(v1, v2, v3); |
||||
|
||||
// create the result image
|
||||
Format imageFormat = texture.getImage().getFormat(); |
||||
int imageWidth = (int) (envelope.width * blenderContext.getBlenderKey().getGeneratedTexturePPU()); |
||||
if (imageWidth == 0) { |
||||
imageWidth = 1; |
||||
} |
||||
int imageHeight = (int) (envelope.height * blenderContext.getBlenderKey().getGeneratedTexturePPU()); |
||||
if (imageHeight == 0) { |
||||
imageHeight = 1; |
||||
} |
||||
ByteBuffer data = BufferUtils.createByteBuffer(imageWidth * imageHeight * (imageFormat.getBitsPerPixel() >> 3)); |
||||
image = new Image(texture.getImage().getFormat(), imageWidth, imageHeight, data, ColorSpace.Linear); |
||||
|
||||
// computing the pixels
|
||||
PixelInputOutput pixelWriter = PixelIOFactory.getPixelIO(imageFormat); |
||||
TexturePixel pixel = new TexturePixel(); |
||||
float[] uvs = new float[3]; |
||||
Vector3f point = new Vector3f(envelope.min); |
||||
Vector3f vecY = new Vector3f(); |
||||
Vector3f wDelta = new Vector3f(envelope.w).multLocal(1.0f / imageWidth); |
||||
Vector3f hDelta = new Vector3f(envelope.h).multLocal(1.0f / imageHeight); |
||||
for (int x = 0; x < imageWidth; ++x) { |
||||
for (int y = 0; y < imageHeight; ++y) { |
||||
this.toTextureUV(boundingBox, point, uvs); |
||||
texture.getPixel(pixel, uvs[0], uvs[1], uvs[2]); |
||||
pixelWriter.write(image, 0, pixel, x, y); |
||||
point.addLocal(hDelta); |
||||
} |
||||
|
||||
vecY.addLocal(wDelta); |
||||
point.set(envelope.min).addLocal(vecY); |
||||
} |
||||
|
||||
// preparing UV coordinates for the flatted texture
|
||||
this.uv = new Vector2f[3]; |
||||
this.uv[0] = new Vector2f(FastMath.clamp(v1.subtract(envelope.min).length(), 0, Float.MAX_VALUE) / envelope.height, 0); |
||||
Vector3f heightDropPoint = v2.subtract(envelope.w);// w is directed from the base to v2
|
||||
this.uv[1] = new Vector2f(1, heightDropPoint.subtractLocal(envelope.min).length() / envelope.height); |
||||
this.uv[2] = new Vector2f(0, 1); |
||||
} |
||||
|
||||
/** |
||||
* This method computes the final UV coordinates for the image (after it |
||||
* is combined with other images and drawed on the result image). |
||||
* |
||||
* @param totalImageWidth |
||||
* the result image width |
||||
* @param totalImageHeight |
||||
* the result image height |
||||
* @param xPos |
||||
* the most left x coordinate of the image |
||||
* @param yPos |
||||
* the most top y coordinate of the image |
||||
* @param result |
||||
* a vector where the result is stored |
||||
*/ |
||||
public void computeFinalUVCoordinates(int totalImageWidth, int totalImageHeight, int xPos, int yPos, Vector2f[] result) { |
||||
for (int i = 0; i < 3; ++i) { |
||||
result[i] = new Vector2f(); |
||||
result[i].x = xPos / (float) totalImageWidth + uv[i].x * (image.getWidth() / (float) totalImageWidth); |
||||
result[i].y = yPos / (float) totalImageHeight + uv[i].y * (image.getHeight() / (float) totalImageHeight); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method converts the given point into 3D UV coordinates. |
||||
* |
||||
* @param boundingBox |
||||
* the bounding box of the mesh |
||||
* @param point |
||||
* the point to be transformed |
||||
* @param uvs |
||||
* the result UV coordinates |
||||
*/ |
||||
private void toTextureUV(BoundingBox boundingBox, Vector3f point, float[] uvs) { |
||||
uvs[0] = (point.x - boundingBox.getCenter().x) / (boundingBox.getXExtent() == 0 ? 1 : boundingBox.getXExtent()); |
||||
uvs[1] = (point.y - boundingBox.getCenter().y) / (boundingBox.getYExtent() == 0 ? 1 : boundingBox.getYExtent()); |
||||
uvs[2] = (point.z - boundingBox.getCenter().z) / (boundingBox.getZExtent() == 0 ? 1 : boundingBox.getZExtent()); |
||||
// UVS cannot go outside <0, 1> range, but since we are generating texture for triangle envelope it might happen that
|
||||
// some points of the envelope will exceet the bounding box of the mesh thus generating uvs outside the range
|
||||
for (int i = 0; i < 3; ++i) { |
||||
uvs[i] = FastMath.clamp(uvs[i], 0, 1); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method returns an envelope of a minimal rectangle, that is set |
||||
* in 3D space, and contains the given triangle. |
||||
* |
||||
* @param triangle |
||||
* the triangle |
||||
* @return a rectangle minimum and maximum point and height and width |
||||
*/ |
||||
private RectangleEnvelope getTriangleEnvelope(Vector3f v1, Vector3f v2, Vector3f v3) { |
||||
Vector3f h = v3.subtract(v1);// the height of the resulting rectangle
|
||||
Vector3f temp = v2.subtract(v1); |
||||
|
||||
float field = 0.5f * h.cross(temp).length();// the field of the rectangle: Field = 0.5 * ||h x temp||
|
||||
if (field <= 0.0f) { |
||||
return new RectangleEnvelope(v1);// return single point envelope
|
||||
} |
||||
|
||||
float cosAlpha = h.dot(temp) / (h.length() * temp.length());// the cosinus of angle betweenh and temp
|
||||
|
||||
float triangleHeight = 2 * field / h.length();// the base of the height is the h vector
|
||||
// now calculate the distance between v1 vertex and the point where
|
||||
// the above calculated height 'touches' the base line (it can be
|
||||
// settled outside the h vector)
|
||||
float x = Math.abs((float) Math.sqrt(FastMath.clamp(temp.lengthSquared() - triangleHeight * triangleHeight, 0, Float.MAX_VALUE))) * Math.signum(cosAlpha); |
||||
// now get the height base point
|
||||
Vector3f xPoint = v1.add(h.normalize().multLocal(x)); |
||||
|
||||
// get the minimum point of the envelope
|
||||
Vector3f min = x < 0 ? xPoint : v1; |
||||
if (x < 0) { |
||||
h = v3.subtract(min); |
||||
} else if (x > h.length()) { |
||||
h = xPoint.subtract(min); |
||||
} |
||||
|
||||
Vector3f envelopeWidth = v2.subtract(xPoint); |
||||
return new RectangleEnvelope(min, envelopeWidth, h); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A class that represents a flat rectangle in 3D space that is built on a |
||||
* triangle in 3D space. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class RectangleEnvelope { |
||||
/** The minimum point of the rectangle. */ |
||||
public final Vector3f min; |
||||
/** The width vector. */ |
||||
public final Vector3f w; |
||||
/** The height vector. */ |
||||
public final Vector3f h; |
||||
/** The width of the rectangle. */ |
||||
public final float width; |
||||
/** The height of the rectangle. */ |
||||
public final float height; |
||||
|
||||
/** |
||||
* Constructs a rectangle that actually holds a point, not a triangle. |
||||
* This is a special case that is sometimes used when generating a |
||||
* texture where UV coordinates are defined by normals instead of |
||||
* vertices. |
||||
* |
||||
* @param pointPosition |
||||
* a position in 3D space |
||||
*/ |
||||
public RectangleEnvelope(Vector3f pointPosition) { |
||||
min = pointPosition; |
||||
h = w = Vector3f.ZERO; |
||||
width = height = 1; |
||||
} |
||||
|
||||
/** |
||||
* Constructs a rectangle envelope. |
||||
* |
||||
* @param min |
||||
* the minimum rectangle point |
||||
* @param w |
||||
* the width vector |
||||
* @param h |
||||
* the height vector |
||||
*/ |
||||
public RectangleEnvelope(Vector3f min, Vector3f w, Vector3f h) { |
||||
this.min = min; |
||||
this.h = h; |
||||
this.w = w; |
||||
width = w.length(); |
||||
height = h.length(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "Envelope[min = " + min + ", w = " + w + ", h = " + h + "]"; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Texture createSimpleClone() { |
||||
return null; |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue