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