• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.graphics.g3d;
18 
19 import com.badlogic.gdx.assets.loaders.ModelLoader;
20 import com.badlogic.gdx.graphics.GL20;
21 import com.badlogic.gdx.graphics.Mesh;
22 import com.badlogic.gdx.graphics.Texture;
23 import com.badlogic.gdx.graphics.VertexAttributes;
24 import com.badlogic.gdx.graphics.g3d.attributes.BlendingAttribute;
25 import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
26 import com.badlogic.gdx.graphics.g3d.attributes.FloatAttribute;
27 import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
28 import com.badlogic.gdx.graphics.g3d.model.Animation;
29 import com.badlogic.gdx.graphics.g3d.model.MeshPart;
30 import com.badlogic.gdx.graphics.g3d.model.Node;
31 import com.badlogic.gdx.graphics.g3d.model.NodeAnimation;
32 import com.badlogic.gdx.graphics.g3d.model.NodeKeyframe;
33 import com.badlogic.gdx.graphics.g3d.model.NodePart;
34 import com.badlogic.gdx.graphics.g3d.model.data.ModelAnimation;
35 import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
36 import com.badlogic.gdx.graphics.g3d.model.data.ModelMaterial;
37 import com.badlogic.gdx.graphics.g3d.model.data.ModelMesh;
38 import com.badlogic.gdx.graphics.g3d.model.data.ModelMeshPart;
39 import com.badlogic.gdx.graphics.g3d.model.data.ModelNode;
40 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeAnimation;
41 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodeKeyframe;
42 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodePart;
43 import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture;
44 import com.badlogic.gdx.graphics.g3d.utils.TextureDescriptor;
45 import com.badlogic.gdx.graphics.g3d.utils.TextureProvider;
46 import com.badlogic.gdx.graphics.g3d.utils.TextureProvider.FileTextureProvider;
47 import com.badlogic.gdx.math.Matrix4;
48 import com.badlogic.gdx.math.Quaternion;
49 import com.badlogic.gdx.math.Vector3;
50 import com.badlogic.gdx.math.collision.BoundingBox;
51 import com.badlogic.gdx.utils.Array;
52 import com.badlogic.gdx.utils.ArrayMap;
53 import com.badlogic.gdx.utils.BufferUtils;
54 import com.badlogic.gdx.utils.Disposable;
55 import com.badlogic.gdx.utils.GdxRuntimeException;
56 import com.badlogic.gdx.utils.ObjectMap;
57 
58 /** A model represents a 3D assets. It stores a hierarchy of nodes. A node has a transform and optionally a graphical part in form
59  * of a {@link MeshPart} and {@link Material}. Mesh parts reference subsets of vertices in one of the meshes of the model.
60  * Animations can be applied to nodes, to modify their transform (translation, rotation, scale) over time.</p>
61  *
62  * A model can be rendered by creating a {@link ModelInstance} from it. That instance has an additional transform to position the
63  * model in the world, and allows modification of materials and nodes without destroying the original model. The original model is
64  * the owner of any meshes and textures, all instances created from the model share these resources. Disposing the model will
65  * automatically make all instances invalid!</p>
66  *
67  * A model is created from {@link ModelData}, which in turn is loaded by a {@link ModelLoader}.
68  *
69  * @author badlogic, xoppa */
70 public class Model implements Disposable {
71 	/** the materials of the model, used by nodes that have a graphical representation FIXME not sure if superfluous, allows
72 	 * modification of materials without having to traverse the nodes **/
73 	public final Array<Material> materials = new Array();
74 	/** root nodes of the model **/
75 	public final Array<Node> nodes = new Array();
76 	/** animations of the model, modifying node transformations **/
77 	public final Array<Animation> animations = new Array();
78 	/** the meshes of the model **/
79 	public final Array<Mesh> meshes = new Array();
80 	/** parts of meshes, used by nodes that have a graphical representation FIXME not sure if superfluous, stored in Nodes as well,
81 	 * could be useful to create bullet meshes **/
82 	public final Array<MeshPart> meshParts = new Array();
83 	/** Array of disposable resources like textures or meshes the Model is responsible for disposing **/
84 	protected final Array<Disposable> disposables = new Array();
85 
86 	/** Constructs an empty model. Manual created models do not manage their resources by default. Use
87 	 * {@link #manageDisposable(Disposable)} to add resources to be managed by this model. */
Model()88 	public Model () {
89 	}
90 
91 	/** Constructs a new Model based on the {@link ModelData}. Texture files will be loaded from the internal file storage via an
92 	 * {@link FileTextureProvider}.
93 	 * @param modelData the {@link ModelData} got from e.g. {@link ModelLoader} */
Model(ModelData modelData)94 	public Model (ModelData modelData) {
95 		this(modelData, new FileTextureProvider());
96 	}
97 
98 	/** Constructs a new Model based on the {@link ModelData}.
99 	 * @param modelData the {@link ModelData} got from e.g. {@link ModelLoader}
100 	 * @param textureProvider the {@link TextureProvider} to use for loading the textures */
Model(ModelData modelData, TextureProvider textureProvider)101 	public Model (ModelData modelData, TextureProvider textureProvider) {
102 		load(modelData, textureProvider);
103 	}
104 
load(ModelData modelData, TextureProvider textureProvider)105 	protected void load (ModelData modelData, TextureProvider textureProvider) {
106 		loadMeshes(modelData.meshes);
107 		loadMaterials(modelData.materials, textureProvider);
108 		loadNodes(modelData.nodes);
109 		loadAnimations(modelData.animations);
110 		calculateTransforms();
111 	}
112 
loadAnimations(Iterable<ModelAnimation> modelAnimations)113 	protected void loadAnimations (Iterable<ModelAnimation> modelAnimations) {
114 		for (final ModelAnimation anim : modelAnimations) {
115 			Animation animation = new Animation();
116 			animation.id = anim.id;
117 			for (ModelNodeAnimation nanim : anim.nodeAnimations) {
118 				final Node node = getNode(nanim.nodeId);
119 				if (node == null) continue;
120 				NodeAnimation nodeAnim = new NodeAnimation();
121 				nodeAnim.node = node;
122 
123 				if (nanim.translation != null) {
124 					nodeAnim.translation = new Array<NodeKeyframe<Vector3>>();
125 					nodeAnim.translation.ensureCapacity(nanim.translation.size);
126 					for (ModelNodeKeyframe<Vector3> kf : nanim.translation) {
127 						if (kf.keytime > animation.duration) animation.duration = kf.keytime;
128 						nodeAnim.translation.add(new NodeKeyframe<Vector3>(kf.keytime, new Vector3(kf.value == null ? node.translation
129 							: kf.value)));
130 					}
131 				}
132 
133 				if (nanim.rotation != null) {
134 					nodeAnim.rotation = new Array<NodeKeyframe<Quaternion>>();
135 					nodeAnim.rotation.ensureCapacity(nanim.rotation.size);
136 					for (ModelNodeKeyframe<Quaternion> kf : nanim.rotation) {
137 						if (kf.keytime > animation.duration) animation.duration = kf.keytime;
138 						nodeAnim.rotation.add(new NodeKeyframe<Quaternion>(kf.keytime, new Quaternion(kf.value == null ? node.rotation
139 							: kf.value)));
140 					}
141 				}
142 
143 				if (nanim.scaling != null) {
144 					nodeAnim.scaling = new Array<NodeKeyframe<Vector3>>();
145 					nodeAnim.scaling.ensureCapacity(nanim.scaling.size);
146 					for (ModelNodeKeyframe<Vector3> kf : nanim.scaling) {
147 						if (kf.keytime > animation.duration) animation.duration = kf.keytime;
148 						nodeAnim.scaling.add(new NodeKeyframe<Vector3>(kf.keytime,
149 							new Vector3(kf.value == null ? node.scale : kf.value)));
150 					}
151 				}
152 
153 				if ((nodeAnim.translation != null && nodeAnim.translation.size > 0)
154 					|| (nodeAnim.rotation != null && nodeAnim.rotation.size > 0)
155 					|| (nodeAnim.scaling != null && nodeAnim.scaling.size > 0)) animation.nodeAnimations.add(nodeAnim);
156 			}
157 			if (animation.nodeAnimations.size > 0) animations.add(animation);
158 		}
159 	}
160 
161 	private ObjectMap<NodePart, ArrayMap<String, Matrix4>> nodePartBones = new ObjectMap<NodePart, ArrayMap<String, Matrix4>>();
162 
loadNodes(Iterable<ModelNode> modelNodes)163 	protected void loadNodes (Iterable<ModelNode> modelNodes) {
164 		nodePartBones.clear();
165 		for (ModelNode node : modelNodes) {
166 			nodes.add(loadNode(node));
167 		}
168 		for (ObjectMap.Entry<NodePart, ArrayMap<String, Matrix4>> e : nodePartBones.entries()) {
169 			if (e.key.invBoneBindTransforms == null)
170 				e.key.invBoneBindTransforms = new ArrayMap<Node, Matrix4>(Node.class, Matrix4.class);
171 			e.key.invBoneBindTransforms.clear();
172 			for (ObjectMap.Entry<String, Matrix4> b : e.value.entries())
173 				e.key.invBoneBindTransforms.put(getNode(b.key), new Matrix4(b.value).inv());
174 		}
175 	}
176 
loadNode(ModelNode modelNode)177 	protected Node loadNode (ModelNode modelNode) {
178 		Node node = new Node();
179 		node.id = modelNode.id;
180 
181 		if (modelNode.translation != null) node.translation.set(modelNode.translation);
182 		if (modelNode.rotation != null) node.rotation.set(modelNode.rotation);
183 		if (modelNode.scale != null) node.scale.set(modelNode.scale);
184 		// FIXME create temporary maps for faster lookup?
185 		if (modelNode.parts != null) {
186 			for (ModelNodePart modelNodePart : modelNode.parts) {
187 				MeshPart meshPart = null;
188 				Material meshMaterial = null;
189 
190 				if (modelNodePart.meshPartId != null) {
191 					for (MeshPart part : meshParts) {
192 						if (modelNodePart.meshPartId.equals(part.id)) {
193 							meshPart = part;
194 							break;
195 						}
196 					}
197 				}
198 
199 				if (modelNodePart.materialId != null) {
200 					for (Material material : materials) {
201 						if (modelNodePart.materialId.equals(material.id)) {
202 							meshMaterial = material;
203 							break;
204 						}
205 					}
206 				}
207 
208 				if (meshPart == null || meshMaterial == null) throw new GdxRuntimeException("Invalid node: " + node.id);
209 
210 				if (meshPart != null && meshMaterial != null) {
211 					NodePart nodePart = new NodePart();
212 					nodePart.meshPart = meshPart;
213 					nodePart.material = meshMaterial;
214 					node.parts.add(nodePart);
215 					if (modelNodePart.bones != null) nodePartBones.put(nodePart, modelNodePart.bones);
216 				}
217 			}
218 		}
219 
220 		if (modelNode.children != null) {
221 			for (ModelNode child : modelNode.children) {
222 				node.addChild(loadNode(child));
223 			}
224 		}
225 
226 		return node;
227 	}
228 
loadMeshes(Iterable<ModelMesh> meshes)229 	protected void loadMeshes (Iterable<ModelMesh> meshes) {
230 		for (ModelMesh mesh : meshes) {
231 			convertMesh(mesh);
232 		}
233 	}
234 
convertMesh(ModelMesh modelMesh)235 	protected void convertMesh (ModelMesh modelMesh) {
236 		int numIndices = 0;
237 		for (ModelMeshPart part : modelMesh.parts) {
238 			numIndices += part.indices.length;
239 		}
240 		VertexAttributes attributes = new VertexAttributes(modelMesh.attributes);
241 		int numVertices = modelMesh.vertices.length / (attributes.vertexSize / 4);
242 
243 		Mesh mesh = new Mesh(true, numVertices, numIndices, attributes);
244 		meshes.add(mesh);
245 		disposables.add(mesh);
246 
247 		BufferUtils.copy(modelMesh.vertices, mesh.getVerticesBuffer(), modelMesh.vertices.length, 0);
248 		int offset = 0;
249 		mesh.getIndicesBuffer().clear();
250 		for (ModelMeshPart part : modelMesh.parts) {
251 			MeshPart meshPart = new MeshPart();
252 			meshPart.id = part.id;
253 			meshPart.primitiveType = part.primitiveType;
254 			meshPart.offset = offset;
255 			meshPart.size = part.indices.length;
256 			meshPart.mesh = mesh;
257 			mesh.getIndicesBuffer().put(part.indices);
258 			offset += meshPart.size;
259 			meshParts.add(meshPart);
260 		}
261 		mesh.getIndicesBuffer().position(0);
262 		for (MeshPart part : meshParts)
263 			part.update();
264 	}
265 
loadMaterials(Iterable<ModelMaterial> modelMaterials, TextureProvider textureProvider)266 	protected void loadMaterials (Iterable<ModelMaterial> modelMaterials, TextureProvider textureProvider) {
267 		for (ModelMaterial mtl : modelMaterials) {
268 			this.materials.add(convertMaterial(mtl, textureProvider));
269 		}
270 	}
271 
convertMaterial(ModelMaterial mtl, TextureProvider textureProvider)272 	protected Material convertMaterial (ModelMaterial mtl, TextureProvider textureProvider) {
273 		Material result = new Material();
274 		result.id = mtl.id;
275 		if (mtl.ambient != null) result.set(new ColorAttribute(ColorAttribute.Ambient, mtl.ambient));
276 		if (mtl.diffuse != null) result.set(new ColorAttribute(ColorAttribute.Diffuse, mtl.diffuse));
277 		if (mtl.specular != null) result.set(new ColorAttribute(ColorAttribute.Specular, mtl.specular));
278 		if (mtl.emissive != null) result.set(new ColorAttribute(ColorAttribute.Emissive, mtl.emissive));
279 		if (mtl.reflection != null) result.set(new ColorAttribute(ColorAttribute.Reflection, mtl.reflection));
280 		if (mtl.shininess > 0f) result.set(new FloatAttribute(FloatAttribute.Shininess, mtl.shininess));
281 		if (mtl.opacity != 1.f) result.set(new BlendingAttribute(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, mtl.opacity));
282 
283 		ObjectMap<String, Texture> textures = new ObjectMap<String, Texture>();
284 
285 		// FIXME uvScaling/uvTranslation totally ignored
286 		if (mtl.textures != null) {
287 			for (ModelTexture tex : mtl.textures) {
288 				Texture texture;
289 				if (textures.containsKey(tex.fileName)) {
290 					texture = textures.get(tex.fileName);
291 				} else {
292 					texture = textureProvider.load(tex.fileName);
293 					textures.put(tex.fileName, texture);
294 					disposables.add(texture);
295 				}
296 
297 				TextureDescriptor descriptor = new TextureDescriptor(texture);
298 				descriptor.minFilter = texture.getMinFilter();
299 				descriptor.magFilter = texture.getMagFilter();
300 				descriptor.uWrap = texture.getUWrap();
301 				descriptor.vWrap = texture.getVWrap();
302 
303 				float offsetU = tex.uvTranslation == null ? 0f : tex.uvTranslation.x;
304 				float offsetV = tex.uvTranslation == null ? 0f : tex.uvTranslation.y;
305 				float scaleU = tex.uvScaling == null ? 1f : tex.uvScaling.x;
306 				float scaleV = tex.uvScaling == null ? 1f : tex.uvScaling.y;
307 
308 				switch (tex.usage) {
309 				case ModelTexture.USAGE_DIFFUSE:
310 					result.set(new TextureAttribute(TextureAttribute.Diffuse, descriptor, offsetU, offsetV, scaleU, scaleV));
311 					break;
312 				case ModelTexture.USAGE_SPECULAR:
313 					result.set(new TextureAttribute(TextureAttribute.Specular, descriptor, offsetU, offsetV, scaleU, scaleV));
314 					break;
315 				case ModelTexture.USAGE_BUMP:
316 					result.set(new TextureAttribute(TextureAttribute.Bump, descriptor, offsetU, offsetV, scaleU, scaleV));
317 					break;
318 				case ModelTexture.USAGE_NORMAL:
319 					result.set(new TextureAttribute(TextureAttribute.Normal, descriptor, offsetU, offsetV, scaleU, scaleV));
320 					break;
321 				case ModelTexture.USAGE_AMBIENT:
322 					result.set(new TextureAttribute(TextureAttribute.Ambient, descriptor, offsetU, offsetV, scaleU, scaleV));
323 					break;
324 				case ModelTexture.USAGE_EMISSIVE:
325 					result.set(new TextureAttribute(TextureAttribute.Emissive, descriptor, offsetU, offsetV, scaleU, scaleV));
326 					break;
327 				case ModelTexture.USAGE_REFLECTION:
328 					result.set(new TextureAttribute(TextureAttribute.Reflection, descriptor, offsetU, offsetV, scaleU, scaleV));
329 					break;
330 				}
331 			}
332 		}
333 
334 		return result;
335 	}
336 
337 	/** Adds a {@link Disposable} to be managed and disposed by this Model. Can be used to keep track of manually loaded textures
338 	 * for {@link ModelInstance}.
339 	 * @param disposable the Disposable */
manageDisposable(Disposable disposable)340 	public void manageDisposable (Disposable disposable) {
341 		if (!disposables.contains(disposable, true)) disposables.add(disposable);
342 	}
343 
344 	/** @return the {@link Disposable} objects that will be disposed when the {@link #dispose()} method is called. */
getManagedDisposables()345 	public Iterable<Disposable> getManagedDisposables () {
346 		return disposables;
347 	}
348 
349 	@Override
dispose()350 	public void dispose () {
351 		for (Disposable disposable : disposables) {
352 			disposable.dispose();
353 		}
354 	}
355 
356 	/** Calculates the local and world transform of all {@link Node} instances in this model, recursively. First each
357 	 * {@link Node#localTransform} transform is calculated based on the translation, rotation and scale of each Node. Then each
358 	 * {@link Node#calculateWorldTransform()} is calculated, based on the parent's world transform and the local transform of each
359 	 * Node. Finally, the animation bone matrices are updated accordingly.</p>
360 	 *
361 	 * This method can be used to recalculate all transforms if any of the Node's local properties (translation, rotation, scale)
362 	 * was modified. */
calculateTransforms()363 	public void calculateTransforms () {
364 		final int n = nodes.size;
365 		for (int i = 0; i < n; i++) {
366 			nodes.get(i).calculateTransforms(true);
367 		}
368 		for (int i = 0; i < n; i++) {
369 			nodes.get(i).calculateBoneTransforms(true);
370 		}
371 	}
372 
373 	/** Calculate the bounding box of this model instance. This is a potential slow operation, it is advised to cache the result.
374 	 * @param out the {@link BoundingBox} that will be set with the bounds.
375 	 * @return the out parameter for chaining */
calculateBoundingBox(final BoundingBox out)376 	public BoundingBox calculateBoundingBox (final BoundingBox out) {
377 		out.inf();
378 		return extendBoundingBox(out);
379 	}
380 
381 	/** Extends the bounding box with the bounds of this model instance. This is a potential slow operation, it is advised to cache
382 	 * the result.
383 	 * @param out the {@link BoundingBox} that will be extended with the bounds.
384 	 * @return the out parameter for chaining */
extendBoundingBox(final BoundingBox out)385 	public BoundingBox extendBoundingBox (final BoundingBox out) {
386 		final int n = nodes.size;
387 		for (int i = 0; i < n; i++)
388 			nodes.get(i).extendBoundingBox(out);
389 		return out;
390 	}
391 
392 	/** @param id The ID of the animation to fetch (case sensitive).
393 	 * @return The {@link Animation} with the specified id, or null if not available. */
getAnimation(final String id)394 	public Animation getAnimation (final String id) {
395 		return getAnimation(id, true);
396 	}
397 
398 	/** @param id The ID of the animation to fetch.
399 	 * @param ignoreCase whether to use case sensitivity when comparing the animation id.
400 	 * @return The {@link Animation} with the specified id, or null if not available. */
getAnimation(final String id, boolean ignoreCase)401 	public Animation getAnimation (final String id, boolean ignoreCase) {
402 		final int n = animations.size;
403 		Animation animation;
404 		if (ignoreCase) {
405 			for (int i = 0; i < n; i++)
406 				if ((animation = animations.get(i)).id.equalsIgnoreCase(id)) return animation;
407 		} else {
408 			for (int i = 0; i < n; i++)
409 				if ((animation = animations.get(i)).id.equals(id)) return animation;
410 		}
411 		return null;
412 	}
413 
414 	/** @param id The ID of the material to fetch.
415 	 * @return The {@link Material} with the specified id, or null if not available. */
getMaterial(final String id)416 	public Material getMaterial (final String id) {
417 		return getMaterial(id, true);
418 	}
419 
420 	/** @param id The ID of the material to fetch.
421 	 * @param ignoreCase whether to use case sensitivity when comparing the material id.
422 	 * @return The {@link Material} with the specified id, or null if not available. */
getMaterial(final String id, boolean ignoreCase)423 	public Material getMaterial (final String id, boolean ignoreCase) {
424 		final int n = materials.size;
425 		Material material;
426 		if (ignoreCase) {
427 			for (int i = 0; i < n; i++)
428 				if ((material = materials.get(i)).id.equalsIgnoreCase(id)) return material;
429 		} else {
430 			for (int i = 0; i < n; i++)
431 				if ((material = materials.get(i)).id.equals(id)) return material;
432 		}
433 		return null;
434 	}
435 
436 	/** @param id The ID of the node to fetch.
437 	 * @return The {@link Node} with the specified id, or null if not found. */
getNode(final String id)438 	public Node getNode (final String id) {
439 		return getNode(id, true);
440 	}
441 
442 	/** @param id The ID of the node to fetch.
443 	 * @param recursive false to fetch a root node only, true to search the entire node tree for the specified node.
444 	 * @return The {@link Node} with the specified id, or null if not found. */
getNode(final String id, boolean recursive)445 	public Node getNode (final String id, boolean recursive) {
446 		return getNode(id, recursive, false);
447 	}
448 
449 	/** @param id The ID of the node to fetch.
450 	 * @param recursive false to fetch a root node only, true to search the entire node tree for the specified node.
451 	 * @param ignoreCase whether to use case sensitivity when comparing the node id.
452 	 * @return The {@link Node} with the specified id, or null if not found. */
getNode(final String id, boolean recursive, boolean ignoreCase)453 	public Node getNode (final String id, boolean recursive, boolean ignoreCase) {
454 		return Node.getNode(nodes, id, recursive, ignoreCase);
455 	}
456 }
457