• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 	 */
TextureHelper(String blenderVersion, boolean fixUpAxis)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 	 */
getTexture(Structure tex, BlenderContext blenderContext)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 	 */
mergeTextures(List<Texture> sources, MaterialContext materialContext)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 	 */
convertToNormalMapTexture(Texture source, float strengthFactor)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 	 */
getHeight(BufferedImage image, int x, int y)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 	 */
vectorToColor(float x, float y, float z)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 	 */
getTextureFromImage(Structure image, BlenderContext blenderContext)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 	 */
loadTextureFromFile(String name, BlenderContext blenderContext)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
shouldBeLoaded(Structure structure, BlenderContext blenderContext)478 	public boolean shouldBeLoaded(Structure structure, BlenderContext blenderContext) {
479 		return (blenderContext.getBlenderKey().getFeaturesToLoad() & FeaturesToLoad.TEXTURES) != 0;
480 	}
481 }