Home | History | Annotate | Download | only in textures
      1 /*
      2  * Copyright (c) 2009-2010 jMonkeyEngine
      3  * All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are
      7  * met:
      8  *
      9  * * Redistributions of source code must retain the above copyright
     10  *   notice, this list of conditions and the following disclaimer.
     11  *
     12  * * Redistributions in binary form must reproduce the above copyright
     13  *   notice, this list of conditions and the following disclaimer in the
     14  *   documentation and/or other materials provided with the distribution.
     15  *
     16  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
     17  *   may be used to endorse or promote products derived from this software
     18  *   without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     22  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31  */
     32 package com.jme3.scene.plugins.blender.textures;
     33 
     34 import java.awt.color.ColorSpace;
     35 import java.awt.image.BufferedImage;
     36 import java.awt.image.ColorConvertOp;
     37 import java.nio.ByteBuffer;
     38 import java.util.ArrayList;
     39 import java.util.HashMap;
     40 import java.util.List;
     41 import java.util.Map;
     42 import java.util.logging.Level;
     43 import java.util.logging.Logger;
     44 
     45 import jme3tools.converters.ImageToAwt;
     46 
     47 import com.jme3.asset.AssetManager;
     48 import com.jme3.asset.AssetNotFoundException;
     49 import com.jme3.asset.BlenderKey;
     50 import com.jme3.asset.BlenderKey.FeaturesToLoad;
     51 import com.jme3.asset.GeneratedTextureKey;
     52 import com.jme3.asset.TextureKey;
     53 import com.jme3.math.ColorRGBA;
     54 import com.jme3.math.Vector3f;
     55 import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
     56 import com.jme3.scene.plugins.blender.BlenderContext;
     57 import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
     58 import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
     59 import com.jme3.scene.plugins.blender.file.FileBlockHeader;
     60 import com.jme3.scene.plugins.blender.file.Pointer;
     61 import com.jme3.scene.plugins.blender.file.Structure;
     62 import com.jme3.scene.plugins.blender.materials.MaterialContext;
     63 import com.jme3.texture.Image;
     64 import com.jme3.texture.Image.Format;
     65 import com.jme3.texture.Texture;
     66 import com.jme3.texture.Texture.MinFilter;
     67 import com.jme3.texture.Texture.WrapMode;
     68 import com.jme3.texture.Texture2D;
     69 import com.jme3.texture.Texture3D;
     70 import com.jme3.util.BufferUtils;
     71 
     72 /**
     73  * A class that is used in texture calculations.
     74  *
     75  * @author Marcin Roguski
     76  */
     77 public class TextureHelper extends AbstractBlenderHelper {
     78 	private static final Logger	LOGGER				= Logger.getLogger(TextureHelper.class.getName());
     79 
     80 	// texture types
     81 	public static final int		TEX_NONE			= 0;
     82 	public static final int		TEX_CLOUDS			= 1;
     83 	public static final int		TEX_WOOD			= 2;
     84 	public static final int		TEX_MARBLE			= 3;
     85 	public static final int		TEX_MAGIC			= 4;
     86 	public static final int		TEX_BLEND			= 5;
     87 	public static final int		TEX_STUCCI			= 6;
     88 	public static final int		TEX_NOISE			= 7;
     89 	public static final int		TEX_IMAGE			= 8;
     90 	public static final int		TEX_PLUGIN			= 9;
     91 	public static final int		TEX_ENVMAP			= 10;
     92 	public static final int		TEX_MUSGRAVE		= 11;
     93 	public static final int		TEX_VORONOI			= 12;
     94 	public static final int		TEX_DISTNOISE		= 13;
     95 	public static final int 	TEX_POINTDENSITY 	= 14;//v. 25+
     96 	public static final int 	TEX_VOXELDATA 		= 15;//v. 25+
     97 
     98 	// mapto
     99 	public static final int		MAP_COL				= 1;
    100 	public static final int		MAP_NORM			= 2;
    101 	public static final int		MAP_COLSPEC			= 4;
    102 	public static final int		MAP_COLMIR			= 8;
    103 	public static final int		MAP_VARS			= 0xFFF0;
    104 	public static final int		MAP_REF				= 16;
    105 	public static final int		MAP_SPEC			= 32;
    106 	public static final int		MAP_EMIT			= 64;
    107 	public static final int		MAP_ALPHA			= 128;
    108 	public static final int		MAP_HAR				= 256;
    109 	public static final int		MAP_RAYMIRR			= 512;
    110 	public static final int		MAP_TRANSLU			= 1024;
    111 	public static final int		MAP_AMB				= 2048;
    112 	public static final int		MAP_DISPLACE		= 4096;
    113 	public static final int		MAP_WARP			= 8192;
    114 	public static final int		MAP_LAYER			= 16384;
    115 
    116 	protected NoiseGenerator noiseGenerator;
    117 	private Map<Integer, TextureGenerator> textureGenerators = new HashMap<Integer, TextureGenerator>();
    118 
    119 	/**
    120 	 * This constructor parses the given blender version and stores the result.
    121 	 * It creates noise generator and texture generators.
    122 	 *
    123 	 * @param blenderVersion
    124 	 *        the version read from the blend file
    125 	 * @param fixUpAxis
    126      *        a variable that indicates if the Y asxis is the UP axis or not
    127 	 */
    128 	public TextureHelper(String blenderVersion, boolean fixUpAxis) {
    129 		super(blenderVersion, false);
    130 		noiseGenerator = new NoiseGenerator(blenderVersion);
    131 		textureGenerators.put(Integer.valueOf(TEX_BLEND), new TextureGeneratorBlend(noiseGenerator));
    132 		textureGenerators.put(Integer.valueOf(TEX_CLOUDS), new TextureGeneratorClouds(noiseGenerator));
    133 		textureGenerators.put(Integer.valueOf(TEX_DISTNOISE), new TextureGeneratorDistnoise(noiseGenerator));
    134 		textureGenerators.put(Integer.valueOf(TEX_MAGIC), new TextureGeneratorMagic(noiseGenerator));
    135 		textureGenerators.put(Integer.valueOf(TEX_MARBLE), new TextureGeneratorMarble(noiseGenerator));
    136 		textureGenerators.put(Integer.valueOf(TEX_MUSGRAVE), new TextureGeneratorMusgrave(noiseGenerator));
    137 		textureGenerators.put(Integer.valueOf(TEX_NOISE), new TextureGeneratorNoise(noiseGenerator));
    138 		textureGenerators.put(Integer.valueOf(TEX_STUCCI), new TextureGeneratorStucci(noiseGenerator));
    139 		textureGenerators.put(Integer.valueOf(TEX_VORONOI), new TextureGeneratorVoronoi(noiseGenerator));
    140 		textureGenerators.put(Integer.valueOf(TEX_WOOD), new TextureGeneratorWood(noiseGenerator));
    141 	}
    142 
    143 	/**
    144 	 * 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
    145 	 * its blender type.
    146 	 *
    147 	 * @param tex
    148 	 *        texture structure filled with data
    149 	 * @param blenderContext
    150 	 *        the blender context
    151 	 * @return the texture that can be used by JME engine
    152 	 * @throws BlenderFileException
    153 	 *         this exception is thrown when the blend file structure is somehow invalid or corrupted
    154 	 */
    155 	public Texture getTexture(Structure tex, BlenderContext blenderContext) throws BlenderFileException {
    156 		Texture result = (Texture) blenderContext.getLoadedFeature(tex.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
    157 		if (result != null) {
    158 			return result;
    159 		}
    160 		int type = ((Number) tex.getFieldValue("type")).intValue();
    161 		int width = blenderContext.getBlenderKey().getGeneratedTextureWidth();
    162 		int height = blenderContext.getBlenderKey().getGeneratedTextureHeight();
    163 		int depth = blenderContext.getBlenderKey().getGeneratedTextureDepth();
    164 
    165 		switch (type) {
    166 		case TEX_IMAGE:// (it is first because probably this will be most commonly used)
    167 			Pointer pImage = (Pointer) tex.getFieldValue("ima");
    168 			if (pImage.isNotNull()){
    169 				Structure image = pImage.fetchData(blenderContext.getInputStream()).get(0);
    170 				result = this.getTextureFromImage(image, blenderContext);
    171 			}
    172 			break;
    173 		case TEX_CLOUDS:
    174 		case TEX_WOOD:
    175 		case TEX_MARBLE:
    176 		case TEX_MAGIC:
    177 		case TEX_BLEND:
    178 		case TEX_STUCCI:
    179 		case TEX_NOISE:
    180 		case TEX_MUSGRAVE:
    181 		case TEX_VORONOI:
    182 		case TEX_DISTNOISE:
    183 			TextureGenerator textureGenerator = textureGenerators.get(Integer.valueOf(type));
    184 			result = textureGenerator.generate(tex, width, height, depth, blenderContext);
    185 			break;
    186 		case TEX_NONE:// No texture, do nothing
    187 			break;
    188 		case TEX_POINTDENSITY:
    189 			LOGGER.warning("Point density texture loading currently not supported!");
    190 			break;
    191 		case TEX_VOXELDATA:
    192 			LOGGER.warning("Voxel data texture loading currently not supported!");
    193 			break;
    194 		case TEX_PLUGIN:
    195 		case TEX_ENVMAP:// TODO: implement envmap texture
    196 			LOGGER.log(Level.WARNING, "Unsupported texture type: {0} for texture: {1}", new Object[]{type, tex.getName()});
    197 			break;
    198 		default:
    199 			throw new BlenderFileException("Unknown texture type: " + type + " for texture: " + tex.getName());
    200 		}
    201 		if (result != null) {
    202 			result.setName(tex.getName());
    203 			result.setWrap(WrapMode.Repeat);
    204 			// NOTE: Enable mipmaps FOR ALL TEXTURES EVER
    205 			result.setMinFilter(MinFilter.Trilinear);
    206 			if(type != TEX_IMAGE) {//only generated textures should have this key
    207 				result.setKey(new GeneratedTextureKey(tex.getName()));
    208 			}
    209 		}
    210 		return result;
    211 	}
    212 
    213 	/**
    214 	 * This method merges the given textures. The result texture has no alpha
    215 	 * factor (is always opaque).
    216 	 *
    217 	 * @param sources
    218 	 *            the textures to be merged
    219 	 * @param materialContext
    220 	 *            the context of the material
    221 	 * @return merged textures
    222 	 */
    223 	public Texture mergeTextures(List<Texture> sources, MaterialContext materialContext) {
    224 		Texture result = null;
    225 		if(sources!=null && sources.size()>0) {
    226 			if(sources.size() == 1) {
    227 				return sources.get(0);//just return the texture
    228 			}
    229 			//checking the sizes of the textures (tehy should perfectly match)
    230 			int lastTextureWithoutAlphaIndex = 0;
    231 			int width = sources.get(0).getImage().getWidth();
    232 			int height = sources.get(0).getImage().getHeight();
    233 			int depth = sources.get(0).getImage().getDepth();
    234 
    235 			for(Texture source : sources) {
    236 				if(source.getImage().getWidth() != width) {
    237 					throw new IllegalArgumentException("The texture " + source.getName() + " has invalid width! It should be: " + width + '!');
    238 				}
    239 				if(source.getImage().getHeight() != height) {
    240 					throw new IllegalArgumentException("The texture " + source.getName() + " has invalid height! It should be: " + height + '!');
    241 				}
    242 				if(source.getImage().getDepth() != depth) {
    243 					throw new IllegalArgumentException("The texture " + source.getName() + " has invalid depth! It should be: " + depth + '!');
    244 				}
    245 				//support for more formats is not necessary at the moment
    246 				if(source.getImage().getFormat()!=Format.RGB8 && source.getImage().getFormat()!=Format.BGR8) {
    247 					++lastTextureWithoutAlphaIndex;
    248 				}
    249 			}
    250 			if(depth==0) {
    251 				depth = 1;
    252 			}
    253 
    254 			//remove textures before the one without alpha (they will be covered anyway)
    255 			if(lastTextureWithoutAlphaIndex > 0 && lastTextureWithoutAlphaIndex<sources.size()-1) {
    256 				sources = sources.subList(lastTextureWithoutAlphaIndex, sources.size()-1);
    257 			}
    258 			int pixelsAmount = width * height * depth;
    259 
    260 			ByteBuffer data = BufferUtils.createByteBuffer(pixelsAmount * 3);
    261 			TexturePixel resultPixel = new TexturePixel();
    262 			TexturePixel sourcePixel = new TexturePixel();
    263 			ColorRGBA diffuseColor = materialContext.getDiffuseColor();
    264 			for (int i = 0; i < pixelsAmount; ++i) {
    265 				for (int j = 0; j < sources.size(); ++j) {
    266 					Image image = sources.get(j).getImage();
    267 					ByteBuffer sourceData = image.getData(0);
    268 					if(j==0) {
    269 						resultPixel.fromColor(diffuseColor);
    270 						sourcePixel.fromImage(image.getFormat(), sourceData, i);
    271 						resultPixel.merge(sourcePixel);
    272 					} else {
    273 						sourcePixel.fromImage(image.getFormat(), sourceData, i);
    274 						resultPixel.merge(sourcePixel);
    275 					}
    276 				}
    277 				data.put((byte)(255 * resultPixel.red));
    278 				data.put((byte)(255 * resultPixel.green));
    279 				data.put((byte)(255 * resultPixel.blue));
    280 				resultPixel.clear();
    281 			}
    282 
    283 			if(depth==1) {
    284 				result = new Texture2D(new Image(Format.RGB8, width, height, data));
    285 			} else {
    286 				ArrayList<ByteBuffer> arrayData = new ArrayList<ByteBuffer>(1);
    287 				arrayData.add(data);
    288 				result = new Texture3D(new Image(Format.RGB8, width, height, depth, arrayData));
    289 			}
    290 		}
    291 		return result;
    292 	}
    293 
    294 	/**
    295 	 * This method converts the given texture into normal-map texture.
    296 	 * @param source
    297 	 *        the source texture
    298 	 * @param strengthFactor
    299 	 *        the normal strength factor
    300 	 * @return normal-map texture
    301 	 */
    302 	public Texture convertToNormalMapTexture(Texture source, float strengthFactor) {
    303 		Image image = source.getImage();
    304 		BufferedImage sourceImage = ImageToAwt.convert(image, false, false, 0);
    305 		BufferedImage heightMap = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
    306 		BufferedImage bumpMap = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
    307 		ColorConvertOp gscale = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
    308 		gscale.filter(sourceImage, heightMap);
    309 
    310 		Vector3f S = new Vector3f();
    311 		Vector3f T = new Vector3f();
    312 		Vector3f N = new Vector3f();
    313 
    314 		for (int x = 0; x < bumpMap.getWidth(); ++x) {
    315 			for (int y = 0; y < bumpMap.getHeight(); ++y) {
    316 				// generating bump pixel
    317 				S.x = 1;
    318 				S.y = 0;
    319 				S.z = strengthFactor * this.getHeight(heightMap, x + 1, y) - strengthFactor * this.getHeight(heightMap, x - 1, y);
    320 				T.x = 0;
    321 				T.y = 1;
    322 				T.z = strengthFactor * this.getHeight(heightMap, x, y + 1) - strengthFactor * this.getHeight(heightMap, x, y - 1);
    323 
    324 				float den = (float) Math.sqrt(S.z * S.z + T.z * T.z + 1);
    325 				N.x = -S.z;
    326 				N.y = -T.z;
    327 				N.z = 1;
    328 				N.divideLocal(den);
    329 
    330 				// setting thge pixel in the result image
    331 				bumpMap.setRGB(x, y, this.vectorToColor(N.x, N.y, N.z));
    332 			}
    333 		}
    334 		ByteBuffer byteBuffer = BufferUtils.createByteBuffer(image.getWidth() * image.getHeight() * 3);
    335 		ImageToAwt.convert(bumpMap, Format.RGB8, byteBuffer);
    336 		return new Texture2D(new Image(Format.RGB8, image.getWidth(), image.getHeight(), byteBuffer));
    337 	}
    338 
    339 	/**
    340 	 * This method returns the height represented by the specified pixel in the given texture.
    341 	 * The given texture should be a height-map.
    342 	 * @param image
    343 	 *        the height-map texture
    344 	 * @param x
    345 	 *        pixel's X coordinate
    346 	 * @param y
    347 	 *        pixel's Y coordinate
    348 	 * @return height reprezented by the given texture in the specified location
    349 	 */
    350 	protected int getHeight(BufferedImage image, int x, int y) {
    351 		if (x < 0) {
    352 			x = 0;
    353 		} else if (x >= image.getWidth()) {
    354 			x = image.getWidth() - 1;
    355 		}
    356 		if (y < 0) {
    357 			y = 0;
    358 		} else if (y >= image.getHeight()) {
    359 			y = image.getHeight() - 1;
    360 		}
    361 		return image.getRGB(x, y) & 0xff;
    362 	}
    363 
    364 	/**
    365 	 * This method transforms given vector's coordinates into ARGB color (A is always = 255).
    366 	 * @param x X factor of the vector
    367 	 * @param y Y factor of the vector
    368 	 * @param z Z factor of the vector
    369 	 * @return color representation of the given vector
    370 	 */
    371 	protected int vectorToColor(float x, float y, float z) {
    372 		int r = Math.round(255 * (x + 1f) / 2f);
    373 		int g = Math.round(255 * (y + 1f) / 2f);
    374 		int b = Math.round(255 * (z + 1f) / 2f);
    375 		return (255 << 24) + (r << 16) + (g << 8) + b;
    376 	}
    377 
    378 	/**
    379 	 * This class returns a texture read from the file or from packed blender data.
    380 	 *
    381 	 * @param image
    382 	 *        image structure filled with data
    383 	 * @param blenderContext
    384 	 *        the blender context
    385 	 * @return the texture that can be used by JME engine
    386 	 * @throws BlenderFileException
    387 	 *         this exception is thrown when the blend file structure is somehow invalid or corrupted
    388 	 */
    389 	public Texture getTextureFromImage(Structure image, BlenderContext blenderContext) throws BlenderFileException {
    390 		LOGGER.log(Level.FINE, "Fetching texture with OMA = {0}", image.getOldMemoryAddress());
    391 		Texture result = (Texture) blenderContext.getLoadedFeature(image.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
    392 		if (result == null) {
    393 			String texturePath = image.getFieldValue("name").toString();
    394 			Pointer pPackedFile = (Pointer) image.getFieldValue("packedfile");
    395 			if (pPackedFile.isNull()) {
    396 				LOGGER.log(Level.INFO, "Reading texture from file: {0}", texturePath);
    397 				result = this.loadTextureFromFile(texturePath, blenderContext);
    398 			} else {
    399 				LOGGER.info("Packed texture. Reading directly from the blend file!");
    400 				Structure packedFile = pPackedFile.fetchData(blenderContext.getInputStream()).get(0);
    401 				Pointer pData = (Pointer) packedFile.getFieldValue("data");
    402 				FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pData.getOldMemoryAddress());
    403 				blenderContext.getInputStream().setPosition(dataFileBlock.getBlockPosition());
    404 				ImageLoader imageLoader = new ImageLoader();
    405 
    406 				// Should the texture be flipped? It works for sinbad ..
    407 				Image im = imageLoader.loadImage(blenderContext.getInputStream(), dataFileBlock.getBlockPosition(), true);
    408 				if (im != null) {
    409 					result = new Texture2D(im);
    410 				}
    411 			}
    412 			if (result != null) {
    413 				result.setName(texturePath);
    414 				result.setWrap(Texture.WrapMode.Repeat);
    415 				if(LOGGER.isLoggable(Level.FINE)) {
    416 					LOGGER.log(Level.FINE, "Adding texture {0} to the loaded features with OMA = {1}", new Object[] {texturePath, image.getOldMemoryAddress()});
    417 				}
    418 				blenderContext.addLoadedFeatures(image.getOldMemoryAddress(), image.getName(), image, result);
    419 			}
    420 		}
    421 		return result;
    422 	}
    423 
    424 	/**
    425 	 * This method loads the textre from outside the blend file.
    426 	 *
    427 	 * @param name
    428 	 *        the path to the image
    429 	 * @param blenderContext
    430 	 *        the blender context
    431 	 * @return the loaded image or null if the image cannot be found
    432 	 */
    433 	protected Texture loadTextureFromFile(String name, BlenderContext blenderContext) {
    434                 if (!name.contains(".")){
    435                     return null; // no extension means not a valid image
    436                 }
    437 
    438 		AssetManager assetManager = blenderContext.getAssetManager();
    439 		name = name.replaceAll("\\\\", "\\/");
    440 		Texture result = null;
    441 
    442 		List<String> assetNames = new ArrayList<String>();
    443 		if (name.startsWith("//")) {
    444 			String relativePath = name.substring(2);
    445 			//augument the path with blender key path
    446 			BlenderKey blenderKey = blenderContext.getBlenderKey();
    447             int idx = blenderKey.getName().lastIndexOf('/');
    448 			String blenderAssetFolder = blenderKey.getName().substring(0, idx != -1 ? idx : 0);
    449 			assetNames.add(blenderAssetFolder+'/'+relativePath);
    450 		} else {//use every path from the asset name to the root (absolute path)
    451 			String[] paths = name.split("\\/");
    452 			StringBuilder sb = new StringBuilder(paths[paths.length-1]);//the asset name
    453 			assetNames.add(paths[paths.length-1]);
    454 
    455 			for(int i=paths.length-2;i>=0;--i) {
    456 				sb.insert(0, '/');
    457 				sb.insert(0, paths[i]);
    458 				assetNames.add(0, sb.toString());
    459 			}
    460 		}
    461 
    462 		//now try to locate the asset
    463 		for(String assetName : assetNames) {
    464 			try {
    465                 TextureKey key = new TextureKey(assetName);
    466                 key.setGenerateMips(true);
    467                 key.setAsCube(false);
    468 				result = assetManager.loadTexture(key);
    469 				break;//if no exception is thrown then accept the located asset and break the loop
    470 			} catch(AssetNotFoundException e) {
    471 				LOGGER.fine(e.getLocalizedMessage());
    472 			}
    473 		}
    474 		return result;
    475 	}
    476 
    477 	@Override
    478 	public boolean shouldBeLoaded(Structure structure, BlenderContext blenderContext) {
    479 		return (blenderContext.getBlenderKey().getFeaturesToLoad() & FeaturesToLoad.TEXTURES) != 0;
    480 	}
    481 }