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 }