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 */
32package com.jme3.scene.plugins.blender.textures;
33
34import java.awt.color.ColorSpace;
35import java.awt.image.BufferedImage;
36import java.awt.image.ColorConvertOp;
37import java.nio.ByteBuffer;
38import java.util.ArrayList;
39import java.util.HashMap;
40import java.util.List;
41import java.util.Map;
42import java.util.logging.Level;
43import java.util.logging.Logger;
44
45import jme3tools.converters.ImageToAwt;
46
47import com.jme3.asset.AssetManager;
48import com.jme3.asset.AssetNotFoundException;
49import com.jme3.asset.BlenderKey;
50import com.jme3.asset.BlenderKey.FeaturesToLoad;
51import com.jme3.asset.GeneratedTextureKey;
52import com.jme3.asset.TextureKey;
53import com.jme3.math.ColorRGBA;
54import com.jme3.math.Vector3f;
55import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
56import com.jme3.scene.plugins.blender.BlenderContext;
57import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
58import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
59import com.jme3.scene.plugins.blender.file.FileBlockHeader;
60import com.jme3.scene.plugins.blender.file.Pointer;
61import com.jme3.scene.plugins.blender.file.Structure;
62import com.jme3.scene.plugins.blender.materials.MaterialContext;
63import com.jme3.texture.Image;
64import com.jme3.texture.Image.Format;
65import com.jme3.texture.Texture;
66import com.jme3.texture.Texture.MinFilter;
67import com.jme3.texture.Texture.WrapMode;
68import com.jme3.texture.Texture2D;
69import com.jme3.texture.Texture3D;
70import com.jme3.util.BufferUtils;
71
72/**
73 * A class that is used in texture calculations.
74 *
75 * @author Marcin Roguski
76 */
77public 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}