1 package com.badlogic.gdx.graphics.g3d;
3 import com.badlogic.gdx.Gdx;
4 import com.badlogic.gdx.assets.loaders.ModelLoader;
5 import com.badlogic.gdx.files.FileHandle;
6 import com.badlogic.gdx.graphics.GL10;
7 import com.badlogic.gdx.graphics.GL20;
8 import com.badlogic.gdx.graphics.Mesh;
9 import com.badlogic.gdx.graphics.Texture;
10 import com.badlogic.gdx.graphics.VertexAttribute;
11 import com.badlogic.gdx.graphics.VertexAttributes;
12 import com.badlogic.gdx.graphics.g3d.materials.BlendingAttribute;
13 import com.badlogic.gdx.graphics.g3d.materials.ColorAttribute;
14 import com.badlogic.gdx.graphics.g3d.materials.FloatAttribute;
15 import com.badlogic.gdx.graphics.g3d.materials.Material;
16 import com.badlogic.gdx.graphics.g3d.materials.TextureAttribute;
17 import com.badlogic.gdx.graphics.g3d.model.Animation;
18 import com.badlogic.gdx.graphics.g3d.model.MeshPart;
19 import com.badlogic.gdx.graphics.g3d.model.NodeAnimation;
20 import com.badlogic.gdx.graphics.g3d.model.NodeKeyframe;
21 import com.badlogic.gdx.graphics.g3d.model.NodePart;
22 import com.badlogic.gdx.graphics.g3d.model.Node;
23 import com.badlogic.gdx.graphics.g3d.model.data.ModelAnimation;
24 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeAnimation;
25 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeKeyframe;
26 import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
27 import com.badlogic.gdx.graphics.g3d.model.data.ModelMaterial;
28 import com.badlogic.gdx.graphics.g3d.model.data.ModelMesh;
29 import com.badlogic.gdx.graphics.g3d.model.data.ModelMeshPart;
30 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodePart;
31 import com.badlogic.gdx.graphics.g3d.model.data.ModelNode;
32 import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture;
33 import com.badlogic.gdx.graphics.g3d.utils.TextureDescriptor;
34 import com.badlogic.gdx.graphics.g3d.utils.TextureProvider;
35 import com.badlogic.gdx.graphics.g3d.utils.TextureProvider.FileTextureProvider;
36 import com.badlogic.gdx.math.Matrix4;
37 import com.badlogic.gdx.math.collision.BoundingBox;
38 import com.badlogic.gdx.utils.Array;
39 import com.badlogic.gdx.utils.ArrayMap;
40 import com.badlogic.gdx.utils.BufferUtils;
41 import com.badlogic.gdx.utils.Disposable;
42 import com.badlogic.gdx.utils.GdxRuntimeException;
43 import com.badlogic.gdx.utils.ObjectMap;
44 import com.badlogic.gdx.utils.Pool;
47 * A model represents a 3D assets. It stores a hierarchy of nodes. A node has a transform and optionally
48 * a graphical part in form of a {@link MeshPart} and {@link Material}. Mesh parts reference subsets of
49 * vertices in one of the meshes of the model. Animations can be applied to nodes, to modify their
50 * transform (translation, rotation, scale) over time.</p>
52 * A model can be rendered by creating a {@link ModelInstance} from it. That instance has an additional
53 * transform to position the model in the world, and allows modification of materials and nodes without
54 * destroying the original model. The original model is the owner of any meshes and textures, all instances
55 * created from the model share these resources. Disposing the model will automatically make all instances
58 * A model is created from {@link ModelData}, which in turn is loaded by a {@link ModelLoader}.
60 * @author badlogic, xoppa */
61 public class Model implements Disposable {
62 /** 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 **/
63 public final Array<Material> materials = new Array<Material>();
64 /** root nodes of the model **/
65 public final Array<Node> nodes = new Array<Node>();
66 /** animations of the model, modifying node transformations **/
67 public final Array<Animation> animations = new Array<Animation>();
68 /** the meshes of the model **/
69 public final Array<Mesh> meshes = new Array<Mesh>();
70 /** 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 **/
71 public final Array<MeshPart> meshParts = new Array<MeshPart>();
72 /** Array of disposable resources like textures or meshes the Model is responsible for disposing **/
73 protected final Array<Disposable> disposables = new Array<Disposable>();
75 /** Constructs an empty model. Manual created models do not manage their resources by default.
76 * Use {@link #manageDisposable(Disposable)} to add resources to be managed by this model. */
79 /** Constructs a new Model based on the {@link ModelData}. Texture files
80 * will be loaded from the internal file storage via an {@link FileTextureProvider}.
81 * @param modelData the {@link ModelData} got from e.g. {@link ModelLoader} */
82 public Model(ModelData modelData) {
83 this(modelData, new FileTextureProvider());
86 /** Constructs a new Model based on the {@link ModelData}.
87 * @param modelData the {@link ModelData} got from e.g. {@link ModelLoader}
88 * @param textureProvider the {@link TextureProvider} to use for loading the textures */
89 public Model(ModelData modelData, TextureProvider textureProvider) {
90 load(modelData, textureProvider);
93 private void load(ModelData modelData, TextureProvider textureProvider) {
94 loadMeshes(modelData.meshes);
95 loadMaterials(modelData.materials, textureProvider);
96 loadNodes(modelData.nodes);
97 loadAnimations(modelData.animations);
98 calculateTransforms();
101 private void loadAnimations (Iterable<ModelAnimation> modelAnimations) {
102 for (final ModelAnimation anim : modelAnimations) {
103 Animation animation = new Animation();
104 animation.id = anim.id;
105 for (ModelNodeAnimation nanim : anim.nodeAnimations) {
106 final Node node = getNode(nanim.nodeId);
109 NodeAnimation nodeAnim = new NodeAnimation();
110 nodeAnim.node = node;
111 for (ModelNodeKeyframe kf : nanim.keyframes) {
112 if (kf.keytime > animation.duration)
113 animation.duration = kf.keytime;
114 NodeKeyframe keyframe = new NodeKeyframe();
115 keyframe.keytime = kf.keytime;
116 keyframe.rotation.set(kf.rotation == null ? node.rotation : kf.rotation);
117 keyframe.scale.set(kf.scale == null ? node.scale : kf.scale);
118 keyframe.translation.set(kf.translation == null ? node.translation : kf.translation);
119 nodeAnim.keyframes.add(keyframe);
121 if (nodeAnim.keyframes.size > 0)
122 animation.nodeAnimations.add(nodeAnim);
124 if (animation.nodeAnimations.size > 0)
125 animations.add(animation);
129 private ObjectMap<NodePart, ArrayMap<String, Matrix4>> nodePartBones = new ObjectMap<NodePart, ArrayMap<String, Matrix4>>();
130 private void loadNodes (Iterable<ModelNode> modelNodes) {
131 nodePartBones.clear();
132 for(ModelNode node: modelNodes) {
133 nodes.add(loadNode(null, node));
135 for (ObjectMap.Entry<NodePart,ArrayMap<String, Matrix4>> e : nodePartBones.entries()) {
136 if (e.key.invBoneBindTransforms == null)
137 e.key.invBoneBindTransforms = new ArrayMap<Node, Matrix4>(Node.class, Matrix4.class);
138 e.key.invBoneBindTransforms.clear();
139 for (ObjectMap.Entry<String, Matrix4> b : e.value.entries())
140 e.key.invBoneBindTransforms.put(getNode(b.key), new Matrix4(b.value).inv());
144 private Node loadNode (Node parent, ModelNode modelNode) {
145 Node node = new Node();
146 node.id = modelNode.id;
147 node.parent = parent;
149 if (modelNode.translation != null)
150 node.translation.set(modelNode.translation);
151 if (modelNode.rotation != null)
152 node.rotation.set(modelNode.rotation);
153 if (modelNode.scale != null)
154 node.scale.set(modelNode.scale);
155 // FIXME create temporary maps for faster lookup?
156 if (modelNode.parts != null) {
157 for(ModelNodePart modelNodePart: modelNode.parts) {
158 MeshPart meshPart = null;
159 Material meshMaterial = null;
161 if(modelNodePart.meshPartId != null) {
162 for(MeshPart part: meshParts) {
163 if(modelNodePart.meshPartId.equals(part.id)) {
170 if(modelNodePart.materialId != null) {
171 for(Material material: materials) {
172 if(modelNodePart.materialId.equals(material.id)) {
173 meshMaterial = material;
179 if (meshPart == null || meshMaterial == null)
180 throw new GdxRuntimeException("Invalid node: "+node.id);
182 if(meshPart != null && meshMaterial != null) {
183 NodePart nodePart = new NodePart();
184 nodePart.meshPart = meshPart;
185 nodePart.material = meshMaterial;
186 node.parts.add(nodePart);
187 if (modelNodePart.bones != null)
188 nodePartBones.put(nodePart, modelNodePart.bones);
193 if(modelNode.children != null) {
194 for(ModelNode child: modelNode.children) {
195 node.children.add(loadNode(node, child));
202 private void loadMeshes (Iterable<ModelMesh> meshes) {
203 for(ModelMesh mesh: meshes) {
208 private void convertMesh (ModelMesh modelMesh) {
210 for(ModelMeshPart part: modelMesh.parts) {
211 numIndices += part.indices.length;
213 VertexAttributes attributes = new VertexAttributes(modelMesh.attributes);
214 int numVertices = modelMesh.vertices.length / (attributes.vertexSize / 4);
216 Mesh mesh = new Mesh(true, numVertices, numIndices, attributes);
218 disposables.add(mesh);
220 BufferUtils.copy(modelMesh.vertices, mesh.getVerticesBuffer(), modelMesh.vertices.length, 0);
222 mesh.getIndicesBuffer().clear();
223 for(ModelMeshPart part: modelMesh.parts) {
224 MeshPart meshPart = new MeshPart();
225 meshPart.id = part.id;
226 meshPart.primitiveType = part.primitiveType;
227 meshPart.indexOffset = offset;
228 meshPart.numVertices = part.indices.length;
229 meshPart.mesh = mesh;
230 mesh.getIndicesBuffer().put(part.indices);
231 offset += meshPart.numVertices;
232 meshParts.add(meshPart);
234 mesh.getIndicesBuffer().position(0);
237 private void loadMaterials (Iterable<ModelMaterial> modelMaterials, TextureProvider textureProvider) {
238 for(ModelMaterial mtl: modelMaterials) {
239 this.materials.add(convertMaterial(mtl, textureProvider));
243 private Material convertMaterial(ModelMaterial mtl, TextureProvider textureProvider) {
244 Material result = new Material();
246 if (mtl.ambient != null)
247 result.set(new ColorAttribute(ColorAttribute.Ambient, mtl.ambient));
248 if (mtl.diffuse != null)
249 result.set(new ColorAttribute(ColorAttribute.Diffuse, mtl.diffuse));
250 if (mtl.specular != null)
251 result.set(new ColorAttribute(ColorAttribute.Specular, mtl.specular));
252 if (mtl.emissive != null)
253 result.set(new ColorAttribute(ColorAttribute.Emissive, mtl.emissive));
254 if (mtl.reflection != null)
255 result.set(new ColorAttribute(ColorAttribute.Reflection, mtl.reflection));
256 if (mtl.shininess > 0f)
257 result.set(new FloatAttribute(FloatAttribute.Shininess, mtl.shininess));
258 if (mtl.opacity != 1.f)
259 result.set(new BlendingAttribute(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA, mtl.opacity));
261 ObjectMap<String, Texture> textures = new ObjectMap<String, Texture>();
263 // FIXME mipmapping totally ignored, filters totally ignored, uvScaling/uvTranslation totally ignored
264 if(mtl.textures != null) {
265 for(ModelTexture tex: mtl.textures) {
267 if(textures.containsKey(tex.fileName)) {
268 texture = textures.get(tex.fileName);
270 texture = textureProvider.load(tex.fileName);
271 textures.put(tex.fileName, texture);
272 disposables.add(texture);
275 TextureDescriptor descriptor = new TextureDescriptor(texture);
276 descriptor.minFilter = Texture.TextureFilter.Linear;
277 descriptor.magFilter = Texture.TextureFilter.Linear;
278 descriptor.uWrap = Texture.TextureWrap.Repeat;
279 descriptor.vWrap = Texture.TextureWrap.Repeat;
281 case ModelTexture.USAGE_DIFFUSE:
282 result.set(new TextureAttribute(TextureAttribute.Diffuse, descriptor));
284 case ModelTexture.USAGE_SPECULAR:
285 result.set(new TextureAttribute(TextureAttribute.Specular, descriptor));
287 case ModelTexture.USAGE_BUMP:
288 result.set(new TextureAttribute(TextureAttribute.Bump, descriptor));
290 case ModelTexture.USAGE_NORMAL:
291 result.set(new TextureAttribute(TextureAttribute.Normal, descriptor));
300 /** Adds a {@link Disposable} to be managed and disposed by this Model. Can
301 * be used to keep track of manually loaded textures for {@link ModelInstance}.
302 * @param disposable the Disposable */
303 public void manageDisposable(Disposable disposable) {
304 if (!disposables.contains(disposable, true))
305 disposables.add(disposable);
308 /** @return the {@link Disposable} objects that will be disposed when the {@link #dispose()} method is called. */
309 public Iterable<Disposable> getManagedDisposables() {
314 public void dispose () {
315 for(Disposable disposable: disposables) {
316 disposable.dispose();
320 /** Calculates the local and world transform of all {@link Node} instances in this model, recursively.
321 * First each {@link Node#localTransform} transform is calculated based on the translation, rotation and
322 * scale of each Node. Then each {@link Node#calculateWorldTransform()}
323 * is calculated, based on the parent's world transform and the local transform of each Node.
324 * Finally, the animation bone matrices are updated accordingly.</p>
326 * This method can be used to recalculate all transforms if any of the Node's local properties (translation, rotation, scale)
329 public void calculateTransforms() {
330 final int n = nodes.size;
331 for(int i = 0; i < n; i++) {
332 nodes.get(i).calculateTransforms(true);
334 for(int i = 0; i < n; i++) {
335 nodes.get(i).calculateBoneTransforms(true);
339 /** Calculate the bounding box of this model instance.
340 * This is a potential slow operation, it is advised to cache the result.
341 * @param out the {@link BoundingBox} that will be set with the bounds.
342 * @return the out parameter for chaining */
343 public BoundingBox calculateBoundingBox(final BoundingBox out) {
345 return extendBoundingBox(out);
348 /** Extends the bounding box with the bounds of this model instance.
349 * This is a potential slow operation, it is advised to cache the result.
350 * @param out the {@link BoundingBox} that will be extended with the bounds.
351 * @return the out parameter for chaining */
352 public BoundingBox extendBoundingBox(final BoundingBox out) {
353 final int n = nodes.size;
354 for(int i = 0; i < n; i++)
355 nodes.get(i).extendBoundingBox(out);
359 /** @param id The ID of the animation to fetch (case sensitive).
360 * @return The {@link Animation} with the specified id, or null if not available. */
361 public Animation getAnimation(final String id) {
362 return getAnimation(id, true);
365 /** @param id The ID of the animation to fetch.
366 * @param ignoreCase whether to use case sensitivity when comparing the animation id.
367 * @return The {@link Animation} with the specified id, or null if not available. */
368 public Animation getAnimation(final String id, boolean ignoreCase) {
369 final int n = animations.size;
372 for (int i = 0; i < n; i++)
373 if ((animation = animations.get(i)).id.equalsIgnoreCase(id))
376 for (int i = 0; i < n; i++)
377 if ((animation = animations.get(i)).id.equals(id))
383 /** @param id The ID of the material to fetch.
384 * @return The {@link Material} with the specified id, or null if not available. */
385 public Material getMaterial(final String id) {
386 return getMaterial(id, true);
389 /** @param id The ID of the material to fetch.
390 * @param ignoreCase whether to use case sensitivity when comparing the material id.
391 * @return The {@link Material} with the specified id, or null if not available. */
392 public Material getMaterial(final String id, boolean ignoreCase) {
393 final int n = materials.size;
396 for (int i = 0; i < n; i++)
397 if ((material = materials.get(i)).id.equalsIgnoreCase(id))
400 for (int i = 0; i < n; i++)
401 if ((material = materials.get(i)).id.equals(id))
407 /** @param id The ID of the node to fetch.
408 * @return The {@link Node} with the specified id, or null if not found. */
409 public Node getNode(final String id) {
410 return getNode(id, true);
413 /** @param id The ID of the node to fetch.
414 * @param recursive false to fetch a root node only, true to search the entire node tree for the specified node.
415 * @return The {@link Node} with the specified id, or null if not found. */
416 public Node getNode(final String id, boolean recursive) {
417 return getNode(id, recursive, false);
420 /** @param id The ID of the node to fetch.
421 * @param recursive false to fetch a root node only, true to search the entire node tree for the specified node.
422 * @param ignoreCase whether to use case sensitivity when comparing the node id.
423 * @return The {@link Node} with the specified id, or null if not found. */
424 public Node getNode(final String id, boolean recursive, boolean ignoreCase) {
425 return Node.getNode(nodes, id, recursive, ignoreCase);