blob: 0e0861d669c17d3f52a242fea7c2818d471a5bef [file] [log] [blame]
/*
* Copyright (c) 2009-2010 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.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.nio.ByteBuffer;
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 jme3tools.converters.ImageToAwt;
import com.jme3.asset.AssetManager;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.asset.BlenderKey;
import com.jme3.asset.BlenderKey.FeaturesToLoad;
import com.jme3.asset.GeneratedTextureKey;
import com.jme3.asset.TextureKey;
import com.jme3.math.ColorRGBA;
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.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
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.texture.Image;
import com.jme3.texture.Image.Format;
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.Texture3D;
import com.jme3.util.BufferUtils;
/**
* 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+
// mapto
public static final int MAP_COL = 1;
public static final int MAP_NORM = 2;
public static final int MAP_COLSPEC = 4;
public static final int MAP_COLMIR = 8;
public static final int MAP_VARS = 0xFFF0;
public static final int MAP_REF = 16;
public static final int MAP_SPEC = 32;
public static final int MAP_EMIT = 64;
public static final int MAP_ALPHA = 128;
public static final int MAP_HAR = 256;
public static final int MAP_RAYMIRR = 512;
public static final int MAP_TRANSLU = 1024;
public static final int MAP_AMB = 2048;
public static final int MAP_DISPLACE = 4096;
public static final int MAP_WARP = 8192;
public static final int MAP_LAYER = 16384;
protected NoiseGenerator noiseGenerator;
private Map<Integer, TextureGenerator> textureGenerators = new HashMap<Integer, TextureGenerator>();
/**
* 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 fixUpAxis
* a variable that indicates if the Y asxis is the UP axis or not
*/
public TextureHelper(String blenderVersion, boolean fixUpAxis) {
super(blenderVersion, false);
noiseGenerator = new NoiseGenerator(blenderVersion);
textureGenerators.put(Integer.valueOf(TEX_BLEND), new TextureGeneratorBlend(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_CLOUDS), new TextureGeneratorClouds(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_DISTNOISE), new TextureGeneratorDistnoise(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_MAGIC), new TextureGeneratorMagic(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_MARBLE), new TextureGeneratorMarble(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_MUSGRAVE), new TextureGeneratorMusgrave(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_NOISE), new TextureGeneratorNoise(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_STUCCI), new TextureGeneratorStucci(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_VORONOI), new TextureGeneratorVoronoi(noiseGenerator));
textureGenerators.put(Integer.valueOf(TEX_WOOD), new TextureGeneratorWood(noiseGenerator));
}
/**
* 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 tex
* 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 tex, BlenderContext blenderContext) throws BlenderFileException {
Texture result = (Texture) blenderContext.getLoadedFeature(tex.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
if (result != null) {
return result;
}
int type = ((Number) tex.getFieldValue("type")).intValue();
int width = blenderContext.getBlenderKey().getGeneratedTextureWidth();
int height = blenderContext.getBlenderKey().getGeneratedTextureHeight();
int depth = blenderContext.getBlenderKey().getGeneratedTextureDepth();
switch (type) {
case TEX_IMAGE:// (it is first because probably this will be most commonly used)
Pointer pImage = (Pointer) tex.getFieldValue("ima");
if (pImage.isNotNull()){
Structure image = pImage.fetchData(blenderContext.getInputStream()).get(0);
result = this.getTextureFromImage(image, 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:
TextureGenerator textureGenerator = textureGenerators.get(Integer.valueOf(type));
result = textureGenerator.generate(tex, width, height, depth, blenderContext);
break;
case TEX_NONE:// No texture, do nothing
break;
case TEX_POINTDENSITY:
LOGGER.warning("Point density texture loading currently not supported!");
break;
case TEX_VOXELDATA:
LOGGER.warning("Voxel data texture loading currently not supported!");
break;
case TEX_PLUGIN:
case TEX_ENVMAP:// TODO: implement envmap texture
LOGGER.log(Level.WARNING, "Unsupported texture type: {0} for texture: {1}", new Object[]{type, tex.getName()});
break;
default:
throw new BlenderFileException("Unknown texture type: " + type + " for texture: " + tex.getName());
}
if (result != null) {
result.setName(tex.getName());
result.setWrap(WrapMode.Repeat);
// NOTE: Enable mipmaps FOR ALL TEXTURES EVER
result.setMinFilter(MinFilter.Trilinear);
if(type != TEX_IMAGE) {//only generated textures should have this key
result.setKey(new GeneratedTextureKey(tex.getName()));
}
}
return result;
}
/**
* This method merges the given textures. The result texture has no alpha
* factor (is always opaque).
*
* @param sources
* the textures to be merged
* @param materialContext
* the context of the material
* @return merged textures
*/
public Texture mergeTextures(List<Texture> sources, MaterialContext materialContext) {
Texture result = null;
if(sources!=null && sources.size()>0) {
if(sources.size() == 1) {
return sources.get(0);//just return the texture
}
//checking the sizes of the textures (tehy should perfectly match)
int lastTextureWithoutAlphaIndex = 0;
int width = sources.get(0).getImage().getWidth();
int height = sources.get(0).getImage().getHeight();
int depth = sources.get(0).getImage().getDepth();
for(Texture source : sources) {
if(source.getImage().getWidth() != width) {
throw new IllegalArgumentException("The texture " + source.getName() + " has invalid width! It should be: " + width + '!');
}
if(source.getImage().getHeight() != height) {
throw new IllegalArgumentException("The texture " + source.getName() + " has invalid height! It should be: " + height + '!');
}
if(source.getImage().getDepth() != depth) {
throw new IllegalArgumentException("The texture " + source.getName() + " has invalid depth! It should be: " + depth + '!');
}
//support for more formats is not necessary at the moment
if(source.getImage().getFormat()!=Format.RGB8 && source.getImage().getFormat()!=Format.BGR8) {
++lastTextureWithoutAlphaIndex;
}
}
if(depth==0) {
depth = 1;
}
//remove textures before the one without alpha (they will be covered anyway)
if(lastTextureWithoutAlphaIndex > 0 && lastTextureWithoutAlphaIndex<sources.size()-1) {
sources = sources.subList(lastTextureWithoutAlphaIndex, sources.size()-1);
}
int pixelsAmount = width * height * depth;
ByteBuffer data = BufferUtils.createByteBuffer(pixelsAmount * 3);
TexturePixel resultPixel = new TexturePixel();
TexturePixel sourcePixel = new TexturePixel();
ColorRGBA diffuseColor = materialContext.getDiffuseColor();
for (int i = 0; i < pixelsAmount; ++i) {
for (int j = 0; j < sources.size(); ++j) {
Image image = sources.get(j).getImage();
ByteBuffer sourceData = image.getData(0);
if(j==0) {
resultPixel.fromColor(diffuseColor);
sourcePixel.fromImage(image.getFormat(), sourceData, i);
resultPixel.merge(sourcePixel);
} else {
sourcePixel.fromImage(image.getFormat(), sourceData, i);
resultPixel.merge(sourcePixel);
}
}
data.put((byte)(255 * resultPixel.red));
data.put((byte)(255 * resultPixel.green));
data.put((byte)(255 * resultPixel.blue));
resultPixel.clear();
}
if(depth==1) {
result = new Texture2D(new Image(Format.RGB8, width, height, data));
} else {
ArrayList<ByteBuffer> arrayData = new ArrayList<ByteBuffer>(1);
arrayData.add(data);
result = new Texture3D(new Image(Format.RGB8, width, height, depth, arrayData));
}
}
return result;
}
/**
* 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 Texture convertToNormalMapTexture(Texture source, float strengthFactor) {
Image image = source.getImage();
BufferedImage sourceImage = ImageToAwt.convert(image, 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 * this.getHeight(heightMap, x + 1, y) - strengthFactor * this.getHeight(heightMap, x - 1, y);
T.x = 0;
T.y = 1;
T.z = strengthFactor * this.getHeight(heightMap, x, y + 1) - strengthFactor * this.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 thge pixel in the result image
bumpMap.setRGB(x, y, this.vectorToColor(N.x, N.y, N.z));
}
}
ByteBuffer byteBuffer = BufferUtils.createByteBuffer(image.getWidth() * image.getHeight() * 3);
ImageToAwt.convert(bumpMap, Format.RGB8, byteBuffer);
return new Texture2D(new Image(Format.RGB8, image.getWidth(), image.getHeight(), byteBuffer));
}
/**
* 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 reprezented by the given texture in the specified location
*/
protected 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
*/
protected 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;
}
/**
* This class returns a texture read from the file or from packed blender data.
*
* @param image
* image 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 getTextureFromImage(Structure image, BlenderContext blenderContext) throws BlenderFileException {
LOGGER.log(Level.FINE, "Fetching texture with OMA = {0}", image.getOldMemoryAddress());
Texture result = (Texture) blenderContext.getLoadedFeature(image.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
if (result == null) {
String texturePath = image.getFieldValue("name").toString();
Pointer pPackedFile = (Pointer) image.getFieldValue("packedfile");
if (pPackedFile.isNull()) {
LOGGER.log(Level.INFO, "Reading texture from file: {0}", texturePath);
result = this.loadTextureFromFile(texturePath, blenderContext);
} else {
LOGGER.info("Packed texture. Reading directly from the blend file!");
Structure packedFile = pPackedFile.fetchData(blenderContext.getInputStream()).get(0);
Pointer pData = (Pointer) packedFile.getFieldValue("data");
FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pData.getOldMemoryAddress());
blenderContext.getInputStream().setPosition(dataFileBlock.getBlockPosition());
ImageLoader imageLoader = new ImageLoader();
// Should the texture be flipped? It works for sinbad ..
Image im = imageLoader.loadImage(blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true);
if (im != null) {
result = new Texture2D(im);
}
}
if (result != null) {
result.setName(texturePath);
result.setWrap(Texture.WrapMode.Repeat);
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Adding texture {0} to the loaded features with OMA = {1}", new Object[] {texturePath, image.getOldMemoryAddress()});
}
blenderContext.addLoadedFeatures(image.getOldMemoryAddress(), image.getName(), image, result);
}
}
return result;
}
/**
* This method loads the textre from outside the blend file.
*
* @param name
* the path to the image
* @param blenderContext
* the blender context
* @return the loaded image or null if the image cannot be found
*/
protected Texture loadTextureFromFile(String name, BlenderContext blenderContext) {
if (!name.contains(".")){
return null; // no extension means not a valid image
}
AssetManager assetManager = blenderContext.getAssetManager();
name = name.replaceAll("\\\\", "\\/");
Texture result = null;
List<String> assetNames = new ArrayList<String>();
if (name.startsWith("//")) {
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);
assetNames.add(blenderAssetFolder+'/'+relativePath);
} else {//use every path from the asset name to the root (absolute path)
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.setGenerateMips(true);
key.setAsCube(false);
result = assetManager.loadTexture(key);
break;//if no exception is thrown then accept the located asset and break the loop
} catch(AssetNotFoundException e) {
LOGGER.fine(e.getLocalizedMessage());
}
}
return result;
}
@Override
public boolean shouldBeLoaded(Structure structure, BlenderContext blenderContext) {
return (blenderContext.getBlenderKey().getFeaturesToLoad() & FeaturesToLoad.TEXTURES) != 0;
}
}