/* * Copyright (c) 2009-2012 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; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; 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 com.jme3.animation.Animation; import com.jme3.asset.AssetInfo; import com.jme3.asset.AssetKey; import com.jme3.asset.AssetLoader; import com.jme3.asset.AssetLocator; import com.jme3.asset.AssetManager; import com.jme3.asset.BlenderKey; import com.jme3.asset.ModelKey; import com.jme3.asset.StreamAssetInfo; import com.jme3.light.Light; import com.jme3.math.ColorRGBA; import com.jme3.post.Filter; import com.jme3.renderer.Camera; import com.jme3.scene.CameraNode; import com.jme3.scene.LightNode; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.plugins.blender.animations.AnimationHelper; import com.jme3.scene.plugins.blender.cameras.CameraHelper; import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; import com.jme3.scene.plugins.blender.curves.CurvesHelper; import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderInputStream; import com.jme3.scene.plugins.blender.file.FileBlockHeader; import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode; import com.jme3.scene.plugins.blender.file.Pointer; import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.landscape.LandscapeHelper; import com.jme3.scene.plugins.blender.lights.LightHelper; import com.jme3.scene.plugins.blender.materials.MaterialContext; import com.jme3.scene.plugins.blender.materials.MaterialHelper; import com.jme3.scene.plugins.blender.meshes.MeshHelper; import com.jme3.scene.plugins.blender.meshes.TemporalMesh; import com.jme3.scene.plugins.blender.modifiers.ModifierHelper; import com.jme3.scene.plugins.blender.objects.ObjectHelper; import com.jme3.scene.plugins.blender.particles.ParticlesHelper; import com.jme3.scene.plugins.blender.textures.TextureHelper; import com.jme3.texture.Texture; /** * This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures. * @author Marcin Roguski (Kaelthas) */ public class BlenderLoader implements AssetLoader { private static final Logger LOGGER = Logger.getLogger(BlenderLoader.class.getName()); @Override public Spatial load(AssetInfo assetInfo) throws IOException { try { BlenderContext blenderContext = this.setup(assetInfo); AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); animationHelper.loadAnimations(); BlenderKey blenderKey = blenderContext.getBlenderKey(); LoadedFeatures loadedFeatures = new LoadedFeatures(); for (FileBlockHeader block : blenderContext.getBlocks()) { switch (block.getCode()) { case BLOCK_OB00: ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); Node object = (Node) objectHelper.toObject(block.getStructure(blenderContext), blenderContext); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { object.getName(), object.getLocalTranslation().toString(), object.getParent() == null ? "null" : object.getParent().getName() }); } if (object.getParent() == null) { loadedFeatures.objects.add(object); } if (object instanceof LightNode && ((LightNode) object).getLight() != null) { loadedFeatures.lights.add(((LightNode) object).getLight()); } else if (object instanceof CameraNode && ((CameraNode) object).getCamera() != null) { loadedFeatures.cameras.add(((CameraNode) object).getCamera()); } break; case BLOCK_SC00:// Scene loadedFeatures.sceneBlocks.add(block); break; case BLOCK_MA00:// Material MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class); MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext); loadedFeatures.materials.add(materialContext); break; case BLOCK_ME00:// Mesh MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class); TemporalMesh temporalMesh = meshHelper.toTemporalMesh(block.getStructure(blenderContext), blenderContext); loadedFeatures.meshes.add(temporalMesh); break; case BLOCK_IM00:// Image TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class); Texture image = textureHelper.loadImageAsTexture(block.getStructure(blenderContext), 0, blenderContext); if (image != null && image.getImage() != null) {// render results are stored as images but are not being loaded loadedFeatures.images.add(image); } break; case BLOCK_TE00: Structure textureStructure = block.getStructure(blenderContext); int type = ((Number) textureStructure.getFieldValue("type")).intValue(); if (type == TextureHelper.TEX_IMAGE) { TextureHelper texHelper = blenderContext.getHelper(TextureHelper.class); Texture texture = texHelper.getTexture(textureStructure, null, blenderContext); if (texture != null) {// null is returned when texture has no image loadedFeatures.textures.add(texture); } } else { LOGGER.fine("Only image textures can be loaded as unlinked assets. Generated textures will be applied to an existing object."); } break; case BLOCK_WO00:// World LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class); Structure worldStructure = block.getStructure(blenderContext); String worldName = worldStructure.getName(); if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) { Light ambientLight = landscapeHelper.toAmbientLight(worldStructure); if (ambientLight != null) { loadedFeatures.objects.add(new LightNode(null, ambientLight)); loadedFeatures.lights.add(ambientLight); } loadedFeatures.sky = landscapeHelper.toSky(worldStructure); loadedFeatures.backgroundColor = landscapeHelper.toBackgroundColor(worldStructure); Filter fogFilter = landscapeHelper.toFog(worldStructure); if (fogFilter != null) { loadedFeatures.filters.add(landscapeHelper.toFog(worldStructure)); } } break; case BLOCK_AC00: LOGGER.fine("Loading unlinked animations is not yet supported!"); break; default: LOGGER.log(Level.FINEST, "Ommiting the block: {0}.", block.getCode()); } } LOGGER.fine("Baking constraints after every feature is loaded."); ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); constraintHelper.bakeConstraints(blenderContext); LOGGER.fine("Loading scenes and attaching them to the root object."); for (FileBlockHeader sceneBlock : loadedFeatures.sceneBlocks) { loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext), blenderContext)); } LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene and loaded features to it."); Node modelRoot = new Node(blenderKey.getName()); for (Node scene : loadedFeatures.scenes) { modelRoot.attachChild(scene); } if (blenderKey.isLoadUnlinkedAssets()) { LOGGER.fine("Setting loaded content as user data in resulting sptaial."); Map<String, Map<String, Object>> linkedData = new HashMap<String, Map<String, Object>>(); Map<String, Object> thisFileData = new HashMap<String, Object>(); thisFileData.put("scenes", loadedFeatures.scenes == null ? new ArrayList<Object>() : loadedFeatures.scenes); thisFileData.put("objects", loadedFeatures.objects == null ? new ArrayList<Object>() : loadedFeatures.objects); thisFileData.put("meshes", loadedFeatures.meshes == null ? new ArrayList<Object>() : loadedFeatures.meshes); thisFileData.put("materials", loadedFeatures.materials == null ? new ArrayList<Object>() : loadedFeatures.materials); thisFileData.put("textures", loadedFeatures.textures == null ? new ArrayList<Object>() : loadedFeatures.textures); thisFileData.put("images", loadedFeatures.images == null ? new ArrayList<Object>() : loadedFeatures.images); thisFileData.put("animations", loadedFeatures.animations == null ? new ArrayList<Object>() : loadedFeatures.animations); thisFileData.put("cameras", loadedFeatures.cameras == null ? new ArrayList<Object>() : loadedFeatures.cameras); thisFileData.put("lights", loadedFeatures.lights == null ? new ArrayList<Object>() : loadedFeatures.lights); thisFileData.put("filters", loadedFeatures.filters == null ? new ArrayList<Object>() : loadedFeatures.filters); thisFileData.put("backgroundColor", loadedFeatures.backgroundColor); thisFileData.put("sky", loadedFeatures.sky); linkedData.put("this", thisFileData); linkedData.putAll(blenderContext.getLinkedFeatures()); modelRoot.setUserData("linkedData", linkedData); } return modelRoot; } catch (BlenderFileException e) { throw new IOException(e.getLocalizedMessage(), e); } catch (Exception e) { throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e); } finally { this.clear(assetInfo); } } /** * This method converts the given structure to a scene node. * @param structure * structure of a scene * @param blenderContext the blender context * @return scene's node * @throws BlenderFileException * an exception throw when problems with blender file occur */ private Node toScene(Structure structure, BlenderContext blenderContext) throws BlenderFileException { ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class); Node result = new Node(structure.getName()); List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase(); for (Structure b : base) { Pointer pObject = (Pointer) b.getFieldValue("object"); if (pObject.isNotNull()) { Structure objectStructure = pObject.fetchData().get(0); Object object = objectHelper.toObject(objectStructure, blenderContext); if (object instanceof LightNode) { result.addLight(((LightNode) object).getLight());// FIXME: check if this is needed !!! result.attachChild((LightNode) object); } else if (object instanceof Node) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() }); } if (((Node) object).getParent() == null) { result.attachChild((Spatial) object); } } } } return result; } /** * This method sets up the loader. * @param assetInfo * the asset info * @throws BlenderFileException * an exception is throw when something wrong happens with blender file */ protected BlenderContext setup(AssetInfo assetInfo) throws BlenderFileException { // registering loaders ModelKey modelKey = (ModelKey) assetInfo.getKey(); BlenderKey blenderKey; if (modelKey instanceof BlenderKey) { blenderKey = (BlenderKey) modelKey; } else { blenderKey = new BlenderKey(modelKey.getName()); } // opening stream BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream()); // reading blocks List<FileBlockHeader> blocks = new ArrayList<FileBlockHeader>(); FileBlockHeader fileBlock; BlenderContext blenderContext = new BlenderContext(); blenderContext.setBlenderVersion(inputStream.getVersionNumber()); blenderContext.setAssetManager(assetInfo.getManager()); blenderContext.setInputStream(inputStream); blenderContext.setBlenderKey(blenderKey); // creating helpers blenderContext.putHelper(AnimationHelper.class, new AnimationHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(TextureHelper.class, new TextureHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(MeshHelper.class, new MeshHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(ObjectHelper.class, new ObjectHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(CurvesHelper.class, new CurvesHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(LightHelper.class, new LightHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(CameraHelper.class, new CameraHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(ModifierHelper.class, new ModifierHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(MaterialHelper.class, new MaterialHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext)); blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(inputStream.getVersionNumber(), blenderContext)); // reading the blocks (dna block is automatically saved in the blender context when found) FileBlockHeader sceneFileBlock = null; do { fileBlock = new FileBlockHeader(inputStream, blenderContext); if (!fileBlock.isDnaBlock()) { blocks.add(fileBlock); // save the scene's file block if (fileBlock.getCode() == BlockCode.BLOCK_SC00) { sceneFileBlock = fileBlock; } } } while (!fileBlock.isLastBlock()); if (sceneFileBlock != null) { blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext)); } // adding locator for linked content assetInfo.getManager().registerLocator(assetInfo.getKey().getName(), LinkedContentLocator.class); return blenderContext; } /** * The internal data is only needed during loading so make it unreachable so that the GC can release * that memory (which can be quite large amount). */ protected void clear(AssetInfo assetInfo) { assetInfo.getManager().unregisterLocator(assetInfo.getKey().getName(), LinkedContentLocator.class); } /** * This class holds the loading results according to the given loading flag. * @author Marcin Roguski (Kaelthas) */ private static class LoadedFeatures { private List<FileBlockHeader> sceneBlocks = new ArrayList<FileBlockHeader>(); /** The scenes from the file. */ private List<Node> scenes = new ArrayList<Node>(); /** Objects from all scenes. */ private List<Node> objects = new ArrayList<Node>(); /** All meshes. */ private List<TemporalMesh> meshes = new ArrayList<TemporalMesh>(); /** Materials from all objects. */ private List<MaterialContext> materials = new ArrayList<MaterialContext>(); /** Textures from all objects. */ private List<Texture> textures = new ArrayList<Texture>(); /** The images stored in the blender file. */ private List<Texture> images = new ArrayList<Texture>(); /** Animations of all objects. */ private List<Animation> animations = new ArrayList<Animation>(); /** All cameras from the file. */ private List<Camera> cameras = new ArrayList<Camera>(); /** All lights from the file. */ private List<Light> lights = new ArrayList<Light>(); /** Loaded sky. */ private Spatial sky; /** Scene filters (ie. FOG). */ private List<Filter> filters = new ArrayList<Filter>(); /** * The background color of the render loaded from the horizon color of the world. If no world is used than the gray color * is set to default (as in blender editor. */ private ColorRGBA backgroundColor = ColorRGBA.Gray; } public static class LinkedContentLocator implements AssetLocator { private File rootFolder; @Override public void setRootPath(String rootPath) { rootFolder = new File(rootPath); if(rootFolder.isFile()) { rootFolder = rootFolder.getParentFile(); } } @SuppressWarnings("rawtypes") @Override public AssetInfo locate(AssetManager manager, AssetKey key) { if(key instanceof BlenderKey) { File linkedAbsoluteFile = new File(key.getName()); if(linkedAbsoluteFile.exists() && linkedAbsoluteFile.isFile()) { try { return new StreamAssetInfo(manager, key, new FileInputStream(linkedAbsoluteFile)); } catch (FileNotFoundException e) { return null; } } File linkedFileInCurrentAssetFolder = new File(rootFolder, linkedAbsoluteFile.getName()); if(linkedFileInCurrentAssetFolder.exists() && linkedFileInCurrentAssetFolder.isFile()) { try { return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentAssetFolder)); } catch (FileNotFoundException e) { return null; } } File linkedFileInCurrentFolder = new File(".", linkedAbsoluteFile.getName()); if(linkedFileInCurrentFolder.exists() && linkedFileInCurrentFolder.isFile()) { try { return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentFolder)); } catch (FileNotFoundException e) { return null; } } } return null; } } }