/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.badlogic.gdx.graphics.g3d; import com.badlogic.gdx.assets.loaders.ModelLoader; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.VertexAttributes; import com.badlogic.gdx.graphics.g3d.attributes.BlendingAttribute; import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; import com.badlogic.gdx.graphics.g3d.attributes.FloatAttribute; import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute; import com.badlogic.gdx.graphics.g3d.model.Animation; import com.badlogic.gdx.graphics.g3d.model.MeshPart; import com.badlogic.gdx.graphics.g3d.model.Node; import com.badlogic.gdx.graphics.g3d.model.NodeAnimation; import com.badlogic.gdx.graphics.g3d.model.NodeKeyframe; import com.badlogic.gdx.graphics.g3d.model.NodePart; import com.badlogic.gdx.graphics.g3d.model.data.ModelAnimation; import com.badlogic.gdx.graphics.g3d.model.data.ModelData; import com.badlogic.gdx.graphics.g3d.model.data.ModelMaterial; import com.badlogic.gdx.graphics.g3d.model.data.ModelMesh; import com.badlogic.gdx.graphics.g3d.model.data.ModelMeshPart; import com.badlogic.gdx.graphics.g3d.model.data.ModelNode; import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeAnimation; import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeKeyframe; import com.badlogic.gdx.graphics.g3d.model.data.ModelNodePart; import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture; import com.badlogic.gdx.graphics.g3d.utils.TextureDescriptor; import com.badlogic.gdx.graphics.g3d.utils.TextureProvider; import com.badlogic.gdx.graphics.g3d.utils.TextureProvider.FileTextureProvider; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Quaternion; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.math.collision.BoundingBox; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ArrayMap; import com.badlogic.gdx.utils.BufferUtils; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.ObjectMap; /** A model represents a 3D assets. It stores a hierarchy of nodes. A node has a transform and optionally a graphical part in form * of a {@link MeshPart} and {@link Material}. Mesh parts reference subsets of vertices in one of the meshes of the model. * Animations can be applied to nodes, to modify their transform (translation, rotation, scale) over time.

* * A model can be rendered by creating a {@link ModelInstance} from it. That instance has an additional transform to position the * model in the world, and allows modification of materials and nodes without destroying the original model. The original model is * the owner of any meshes and textures, all instances created from the model share these resources. Disposing the model will * automatically make all instances invalid!

* * A model is created from {@link ModelData}, which in turn is loaded by a {@link ModelLoader}. * * @author badlogic, xoppa */ public class Model implements Disposable { /** the materials of the model, used by nodes that have a graphical representation FIXME not sure if superfluous, allows * modification of materials without having to traverse the nodes **/ public final Array materials = new Array(); /** root nodes of the model **/ public final Array nodes = new Array(); /** animations of the model, modifying node transformations **/ public final Array animations = new Array(); /** the meshes of the model **/ public final Array meshes = new Array(); /** parts of meshes, used by nodes that have a graphical representation FIXME not sure if superfluous, stored in Nodes as well, * could be useful to create bullet meshes **/ public final Array meshParts = new Array(); /** Array of disposable resources like textures or meshes the Model is responsible for disposing **/ protected final Array disposables = new Array(); /** Constructs an empty model. Manual created models do not manage their resources by default. Use * {@link #manageDisposable(Disposable)} to add resources to be managed by this model. */ public Model () { } /** Constructs a new Model based on the {@link ModelData}. Texture files will be loaded from the internal file storage via an * {@link FileTextureProvider}. * @param modelData the {@link ModelData} got from e.g. {@link ModelLoader} */ public Model (ModelData modelData) { this(modelData, new FileTextureProvider()); } /** Constructs a new Model based on the {@link ModelData}. * @param modelData the {@link ModelData} got from e.g. {@link ModelLoader} * @param textureProvider the {@link TextureProvider} to use for loading the textures */ public Model (ModelData modelData, TextureProvider textureProvider) { load(modelData, textureProvider); } protected void load (ModelData modelData, TextureProvider textureProvider) { loadMeshes(modelData.meshes); loadMaterials(modelData.materials, textureProvider); loadNodes(modelData.nodes); loadAnimations(modelData.animations); calculateTransforms(); } protected void loadAnimations (Iterable modelAnimations) { for (final ModelAnimation anim : modelAnimations) { Animation animation = new Animation(); animation.id = anim.id; for (ModelNodeAnimation nanim : anim.nodeAnimations) { final Node node = getNode(nanim.nodeId); if (node == null) continue; NodeAnimation nodeAnim = new NodeAnimation(); nodeAnim.node = node; if (nanim.translation != null) { nodeAnim.translation = new Array>(); nodeAnim.translation.ensureCapacity(nanim.translation.size); for (ModelNodeKeyframe kf : nanim.translation) { if (kf.keytime > animation.duration) animation.duration = kf.keytime; nodeAnim.translation.add(new NodeKeyframe(kf.keytime, new Vector3(kf.value == null ? node.translation : kf.value))); } } if (nanim.rotation != null) { nodeAnim.rotation = new Array>(); nodeAnim.rotation.ensureCapacity(nanim.rotation.size); for (ModelNodeKeyframe kf : nanim.rotation) { if (kf.keytime > animation.duration) animation.duration = kf.keytime; nodeAnim.rotation.add(new NodeKeyframe(kf.keytime, new Quaternion(kf.value == null ? node.rotation : kf.value))); } } if (nanim.scaling != null) { nodeAnim.scaling = new Array>(); nodeAnim.scaling.ensureCapacity(nanim.scaling.size); for (ModelNodeKeyframe kf : nanim.scaling) { if (kf.keytime > animation.duration) animation.duration = kf.keytime; nodeAnim.scaling.add(new NodeKeyframe(kf.keytime, new Vector3(kf.value == null ? node.scale : kf.value))); } } if ((nodeAnim.translation != null && nodeAnim.translation.size > 0) || (nodeAnim.rotation != null && nodeAnim.rotation.size > 0) || (nodeAnim.scaling != null && nodeAnim.scaling.size > 0)) animation.nodeAnimations.add(nodeAnim); } if (animation.nodeAnimations.size > 0) animations.add(animation); } } private ObjectMap> nodePartBones = new ObjectMap>(); protected void loadNodes (Iterable modelNodes) { nodePartBones.clear(); for (ModelNode node : modelNodes) { nodes.add(loadNode(node)); } for (ObjectMap.Entry> e : nodePartBones.entries()) { if (e.key.invBoneBindTransforms == null) e.key.invBoneBindTransforms = new ArrayMap(Node.class, Matrix4.class); e.key.invBoneBindTransforms.clear(); for (ObjectMap.Entry b : e.value.entries()) e.key.invBoneBindTransforms.put(getNode(b.key), new Matrix4(b.value).inv()); } } protected Node loadNode (ModelNode modelNode) { Node node = new Node(); node.id = modelNode.id; if (modelNode.translation != null) node.translation.set(modelNode.translation); if (modelNode.rotation != null) node.rotation.set(modelNode.rotation); if (modelNode.scale != null) node.scale.set(modelNode.scale); // FIXME create temporary maps for faster lookup? if (modelNode.parts != null) { for (ModelNodePart modelNodePart : modelNode.parts) { MeshPart meshPart = null; Material meshMaterial = null; if (modelNodePart.meshPartId != null) { for (MeshPart part : meshParts) { if (modelNodePart.meshPartId.equals(part.id)) { meshPart = part; break; } } } if (modelNodePart.materialId != null) { for (Material material : materials) { if (modelNodePart.materialId.equals(material.id)) { meshMaterial = material; break; } } } if (meshPart == null || meshMaterial == null) throw new GdxRuntimeException("Invalid node: " + node.id); if (meshPart != null && meshMaterial != null) { NodePart nodePart = new NodePart(); nodePart.meshPart = meshPart; nodePart.material = meshMaterial; node.parts.add(nodePart); if (modelNodePart.bones != null) nodePartBones.put(nodePart, modelNodePart.bones); } } } if (modelNode.children != null) { for (ModelNode child : modelNode.children) { node.addChild(loadNode(child)); } } return node; } protected void loadMeshes (Iterable meshes) { for (ModelMesh mesh : meshes) { convertMesh(mesh); } } protected void convertMesh (ModelMesh modelMesh) { int numIndices = 0; for (ModelMeshPart part : modelMesh.parts) { numIndices += part.indices.length; } VertexAttributes attributes = new VertexAttributes(modelMesh.attributes); int numVertices = modelMesh.vertices.length / (attributes.vertexSize / 4); Mesh mesh = new Mesh(true, numVertices, numIndices, attributes); meshes.add(mesh); disposables.add(mesh); BufferUtils.copy(modelMesh.vertices, mesh.getVerticesBuffer(), modelMesh.vertices.length, 0); int offset = 0; mesh.getIndicesBuffer().clear(); for (ModelMeshPart part : modelMesh.parts) { MeshPart meshPart = new MeshPart(); meshPart.id = part.id; meshPart.primitiveType = part.primitiveType; meshPart.offset = offset; meshPart.size = part.indices.length; meshPart.mesh = mesh; mesh.getIndicesBuffer().put(part.indices); offset += meshPart.size; meshParts.add(meshPart); } mesh.getIndicesBuffer().position(0); for (MeshPart part : meshParts) part.update(); } protected void loadMaterials (Iterable modelMaterials, TextureProvider textureProvider) { for (ModelMaterial mtl : modelMaterials) { this.materials.add(convertMaterial(mtl, textureProvider)); } } protected Material convertMaterial (ModelMaterial mtl, TextureProvider textureProvider) { Material result = new Material(); result.id = mtl.id; if (mtl.ambient != null) result.set(new ColorAttribute(ColorAttribute.Ambient, mtl.ambient)); if (mtl.diffuse != null) result.set(new ColorAttribute(ColorAttribute.Diffuse, mtl.diffuse)); if (mtl.specular != null) result.set(new ColorAttribute(ColorAttribute.Specular, mtl.specular)); if (mtl.emissive != null) result.set(new ColorAttribute(ColorAttribute.Emissive, mtl.emissive)); if (mtl.reflection != null) result.set(new ColorAttribute(ColorAttribute.Reflection, mtl.reflection)); if (mtl.shininess > 0f) result.set(new FloatAttribute(FloatAttribute.Shininess, mtl.shininess)); if (mtl.opacity != 1.f) result.set(new BlendingAttribute(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, mtl.opacity)); ObjectMap textures = new ObjectMap(); // FIXME uvScaling/uvTranslation totally ignored if (mtl.textures != null) { for (ModelTexture tex : mtl.textures) { Texture texture; if (textures.containsKey(tex.fileName)) { texture = textures.get(tex.fileName); } else { texture = textureProvider.load(tex.fileName); textures.put(tex.fileName, texture); disposables.add(texture); } TextureDescriptor descriptor = new TextureDescriptor(texture); descriptor.minFilter = texture.getMinFilter(); descriptor.magFilter = texture.getMagFilter(); descriptor.uWrap = texture.getUWrap(); descriptor.vWrap = texture.getVWrap(); float offsetU = tex.uvTranslation == null ? 0f : tex.uvTranslation.x; float offsetV = tex.uvTranslation == null ? 0f : tex.uvTranslation.y; float scaleU = tex.uvScaling == null ? 1f : tex.uvScaling.x; float scaleV = tex.uvScaling == null ? 1f : tex.uvScaling.y; switch (tex.usage) { case ModelTexture.USAGE_DIFFUSE: result.set(new TextureAttribute(TextureAttribute.Diffuse, descriptor, offsetU, offsetV, scaleU, scaleV)); break; case ModelTexture.USAGE_SPECULAR: result.set(new TextureAttribute(TextureAttribute.Specular, descriptor, offsetU, offsetV, scaleU, scaleV)); break; case ModelTexture.USAGE_BUMP: result.set(new TextureAttribute(TextureAttribute.Bump, descriptor, offsetU, offsetV, scaleU, scaleV)); break; case ModelTexture.USAGE_NORMAL: result.set(new TextureAttribute(TextureAttribute.Normal, descriptor, offsetU, offsetV, scaleU, scaleV)); break; case ModelTexture.USAGE_AMBIENT: result.set(new TextureAttribute(TextureAttribute.Ambient, descriptor, offsetU, offsetV, scaleU, scaleV)); break; case ModelTexture.USAGE_EMISSIVE: result.set(new TextureAttribute(TextureAttribute.Emissive, descriptor, offsetU, offsetV, scaleU, scaleV)); break; case ModelTexture.USAGE_REFLECTION: result.set(new TextureAttribute(TextureAttribute.Reflection, descriptor, offsetU, offsetV, scaleU, scaleV)); break; } } } return result; } /** Adds a {@link Disposable} to be managed and disposed by this Model. Can be used to keep track of manually loaded textures * for {@link ModelInstance}. * @param disposable the Disposable */ public void manageDisposable (Disposable disposable) { if (!disposables.contains(disposable, true)) disposables.add(disposable); } /** @return the {@link Disposable} objects that will be disposed when the {@link #dispose()} method is called. */ public Iterable getManagedDisposables () { return disposables; } @Override public void dispose () { for (Disposable disposable : disposables) { disposable.dispose(); } } /** Calculates the local and world transform of all {@link Node} instances in this model, recursively. First each * {@link Node#localTransform} transform is calculated based on the translation, rotation and scale of each Node. Then each * {@link Node#calculateWorldTransform()} is calculated, based on the parent's world transform and the local transform of each * Node. Finally, the animation bone matrices are updated accordingly.

* * This method can be used to recalculate all transforms if any of the Node's local properties (translation, rotation, scale) * was modified. */ public void calculateTransforms () { final int n = nodes.size; for (int i = 0; i < n; i++) { nodes.get(i).calculateTransforms(true); } for (int i = 0; i < n; i++) { nodes.get(i).calculateBoneTransforms(true); } } /** Calculate the bounding box of this model instance. This is a potential slow operation, it is advised to cache the result. * @param out the {@link BoundingBox} that will be set with the bounds. * @return the out parameter for chaining */ public BoundingBox calculateBoundingBox (final BoundingBox out) { out.inf(); return extendBoundingBox(out); } /** Extends the bounding box with the bounds of this model instance. This is a potential slow operation, it is advised to cache * the result. * @param out the {@link BoundingBox} that will be extended with the bounds. * @return the out parameter for chaining */ public BoundingBox extendBoundingBox (final BoundingBox out) { final int n = nodes.size; for (int i = 0; i < n; i++) nodes.get(i).extendBoundingBox(out); return out; } /** @param id The ID of the animation to fetch (case sensitive). * @return The {@link Animation} with the specified id, or null if not available. */ public Animation getAnimation (final String id) { return getAnimation(id, true); } /** @param id The ID of the animation to fetch. * @param ignoreCase whether to use case sensitivity when comparing the animation id. * @return The {@link Animation} with the specified id, or null if not available. */ public Animation getAnimation (final String id, boolean ignoreCase) { final int n = animations.size; Animation animation; if (ignoreCase) { for (int i = 0; i < n; i++) if ((animation = animations.get(i)).id.equalsIgnoreCase(id)) return animation; } else { for (int i = 0; i < n; i++) if ((animation = animations.get(i)).id.equals(id)) return animation; } return null; } /** @param id The ID of the material to fetch. * @return The {@link Material} with the specified id, or null if not available. */ public Material getMaterial (final String id) { return getMaterial(id, true); } /** @param id The ID of the material to fetch. * @param ignoreCase whether to use case sensitivity when comparing the material id. * @return The {@link Material} with the specified id, or null if not available. */ public Material getMaterial (final String id, boolean ignoreCase) { final int n = materials.size; Material material; if (ignoreCase) { for (int i = 0; i < n; i++) if ((material = materials.get(i)).id.equalsIgnoreCase(id)) return material; } else { for (int i = 0; i < n; i++) if ((material = materials.get(i)).id.equals(id)) return material; } return null; } /** @param id The ID of the node to fetch. * @return The {@link Node} with the specified id, or null if not found. */ public Node getNode (final String id) { return getNode(id, true); } /** @param id The ID of the node to fetch. * @param recursive false to fetch a root node only, true to search the entire node tree for the specified node. * @return The {@link Node} with the specified id, or null if not found. */ public Node getNode (final String id, boolean recursive) { return getNode(id, recursive, false); } /** @param id The ID of the node to fetch. * @param recursive false to fetch a root node only, true to search the entire node tree for the specified node. * @param ignoreCase whether to use case sensitivity when comparing the node id. * @return The {@link Node} with the specified id, or null if not found. */ public Node getNode (final String id, boolean recursive, boolean ignoreCase) { return Node.getNode(nodes, id, recursive, ignoreCase); } }