1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package com.jme3.scene; 33 34 import com.jme3.asset.AssetNotFoundException; 35 import com.jme3.bounding.BoundingVolume; 36 import com.jme3.collision.Collidable; 37 import com.jme3.collision.CollisionResults; 38 import com.jme3.export.InputCapsule; 39 import com.jme3.export.JmeExporter; 40 import com.jme3.export.JmeImporter; 41 import com.jme3.export.OutputCapsule; 42 import com.jme3.material.Material; 43 import com.jme3.math.Matrix4f; 44 import com.jme3.math.Transform; 45 import com.jme3.scene.VertexBuffer.Type; 46 import com.jme3.util.TempVars; 47 import java.io.IOException; 48 import java.util.Queue; 49 import java.util.logging.Level; 50 import java.util.logging.Logger; 51 52 /** 53 * <code>Geometry</code> defines a leaf node of the scene graph. The leaf node 54 * contains the geometric data for rendering objects. It manages all rendering 55 * information such as a {@link Material} object to define how the surface 56 * should be shaded and the {@link Mesh} data to contain the actual geometry. 57 * 58 * @author Kirill Vainer 59 */ 60 public class Geometry extends Spatial { 61 62 // Version #1: removed shared meshes. 63 // models loaded with shared mesh will be automatically fixed. 64 public static final int SAVABLE_VERSION = 1; 65 66 private static final Logger logger = Logger.getLogger(Geometry.class.getName()); 67 protected Mesh mesh; 68 protected transient int lodLevel = 0; 69 protected Material material; 70 /** 71 * When true, the geometry's transform will not be applied. 72 */ 73 protected boolean ignoreTransform = false; 74 protected transient Matrix4f cachedWorldMat = new Matrix4f(); 75 /** 76 * used when geometry is batched 77 */ 78 protected BatchNode batchNode = null; 79 /** 80 * the start index of this geom's mesh in the batchNode mesh 81 */ 82 protected int startIndex; 83 /** 84 * the previous transforms of the geometry used to compute world transforms 85 */ 86 protected Transform prevBatchTransforms = null; 87 /** 88 * the cached offset matrix used when the geometry is batched 89 */ 90 protected Matrix4f cachedOffsetMat = null; 91 92 /** 93 * Serialization only. Do not use. 94 */ 95 public Geometry() { 96 } 97 98 /** 99 * Create a geometry node without any mesh data. 100 * Both the mesh and the material are null, the geometry 101 * cannot be rendered until those are set. 102 * 103 * @param name The name of this geometry 104 */ 105 public Geometry(String name) { 106 super(name); 107 } 108 109 /** 110 * Create a geometry node with mesh data. 111 * The material of the geometry is null, it cannot 112 * be rendered until it is set. 113 * 114 * @param name The name of this geometry 115 * @param mesh The mesh data for this geometry 116 */ 117 public Geometry(String name, Mesh mesh) { 118 this(name); 119 if (mesh == null) { 120 throw new NullPointerException(); 121 } 122 123 this.mesh = mesh; 124 } 125 126 /** 127 * @return If ignoreTransform mode is set. 128 * 129 * @see Geometry#setIgnoreTransform(boolean) 130 */ 131 public boolean isIgnoreTransform() { 132 return ignoreTransform; 133 } 134 135 /** 136 * @param ignoreTransform If true, the geometry's transform will not be applied. 137 */ 138 public void setIgnoreTransform(boolean ignoreTransform) { 139 this.ignoreTransform = ignoreTransform; 140 } 141 142 /** 143 * Sets the LOD level to use when rendering the mesh of this geometry. 144 * Level 0 indicates that the default index buffer should be used, 145 * levels [1, LodLevels + 1] represent the levels set on the mesh 146 * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }. 147 * 148 * @param lod The lod level to set 149 */ 150 @Override 151 public void setLodLevel(int lod) { 152 if (mesh.getNumLodLevels() == 0) { 153 throw new IllegalStateException("LOD levels are not set on this mesh"); 154 } 155 156 if (lod < 0 || lod >= mesh.getNumLodLevels()) { 157 throw new IllegalArgumentException("LOD level is out of range: " + lod); 158 } 159 160 lodLevel = lod; 161 } 162 163 /** 164 * Returns the LOD level set with {@link #setLodLevel(int) }. 165 * 166 * @return the LOD level set 167 */ 168 public int getLodLevel() { 169 return lodLevel; 170 } 171 172 /** 173 * Returns this geometry's mesh vertex count. 174 * 175 * @return this geometry's mesh vertex count. 176 * 177 * @see Mesh#getVertexCount() 178 */ 179 public int getVertexCount() { 180 return mesh.getVertexCount(); 181 } 182 183 /** 184 * Returns this geometry's mesh triangle count. 185 * 186 * @return this geometry's mesh triangle count. 187 * 188 * @see Mesh#getTriangleCount() 189 */ 190 public int getTriangleCount() { 191 return mesh.getTriangleCount(); 192 } 193 194 /** 195 * Sets the mesh to use for this geometry when rendering. 196 * 197 * @param mesh the mesh to use for this geometry 198 * 199 * @throws IllegalArgumentException If mesh is null 200 */ 201 public void setMesh(Mesh mesh) { 202 if (mesh == null) { 203 throw new IllegalArgumentException(); 204 } 205 if (isBatched()) { 206 throw new UnsupportedOperationException("Cannot set the mesh of a batched geometry"); 207 } 208 209 this.mesh = mesh; 210 setBoundRefresh(); 211 } 212 213 /** 214 * Returns the mseh to use for this geometry 215 * 216 * @return the mseh to use for this geometry 217 * 218 * @see #setMesh(com.jme3.scene.Mesh) 219 */ 220 public Mesh getMesh() { 221 return mesh; 222 } 223 224 /** 225 * Sets the material to use for this geometry. 226 * 227 * @param material the material to use for this geometry 228 */ 229 @Override 230 public void setMaterial(Material material) { 231 if (isBatched()) { 232 throw new UnsupportedOperationException("Cannot set the material of a batched geometry, change the material of the parent BatchNode."); 233 } 234 this.material = material; 235 } 236 237 /** 238 * Returns the material that is used for this geometry. 239 * 240 * @return the material that is used for this geometry 241 * 242 * @see #setMaterial(com.jme3.material.Material) 243 */ 244 public Material getMaterial() { 245 return material; 246 } 247 248 /** 249 * @return The bounding volume of the mesh, in model space. 250 */ 251 public BoundingVolume getModelBound() { 252 return mesh.getBound(); 253 } 254 255 /** 256 * Updates the bounding volume of the mesh. Should be called when the 257 * mesh has been modified. 258 */ 259 public void updateModelBound() { 260 mesh.updateBound(); 261 setBoundRefresh(); 262 } 263 264 /** 265 * <code>updateWorldBound</code> updates the bounding volume that contains 266 * this geometry. The location of the geometry is based on the location of 267 * all this node's parents. 268 * 269 * @see Spatial#updateWorldBound() 270 */ 271 @Override 272 protected void updateWorldBound() { 273 super.updateWorldBound(); 274 if (mesh == null) { 275 throw new NullPointerException("Geometry: " + getName() + " has null mesh"); 276 } 277 278 if (mesh.getBound() != null) { 279 if (ignoreTransform) { 280 // we do not transform the model bound by the world transform, 281 // just use the model bound as-is 282 worldBound = mesh.getBound().clone(worldBound); 283 } else { 284 worldBound = mesh.getBound().transform(worldTransform, worldBound); 285 } 286 } 287 } 288 289 @Override 290 protected void updateWorldTransforms() { 291 292 super.updateWorldTransforms(); 293 computeWorldMatrix(); 294 295 if (isBatched()) { 296 computeOffsetTransform(); 297 batchNode.updateSubBatch(this); 298 prevBatchTransforms.set(batchNode.getTransforms(this)); 299 300 } 301 // geometry requires lights to be sorted 302 worldLights.sort(true); 303 } 304 305 /** 306 * Batch this geometry, should only be called by the BatchNode. 307 * @param node the batchNode 308 * @param startIndex the starting index of this geometry in the batched mesh 309 */ 310 protected void batch(BatchNode node, int startIndex) { 311 this.batchNode = node; 312 this.startIndex = startIndex; 313 prevBatchTransforms = new Transform(); 314 cachedOffsetMat = new Matrix4f(); 315 setCullHint(CullHint.Always); 316 } 317 318 /** 319 * unBatch this geometry. 320 */ 321 protected void unBatch() { 322 this.startIndex = 0; 323 prevBatchTransforms = null; 324 cachedOffsetMat = null; 325 //once the geometry is removed from the screnegraph the batchNode needs to be rebatched. 326 this.batchNode.setNeedsFullRebatch(true); 327 this.batchNode = null; 328 setCullHint(CullHint.Dynamic); 329 } 330 331 @Override 332 public boolean removeFromParent() { 333 boolean removed = super.removeFromParent(); 334 //if the geometry is batched we also have to unbatch it 335 if (isBatched()) { 336 unBatch(); 337 } 338 return removed; 339 } 340 341 /** 342 * Recomputes the cached offset matrix used when the geometry is batched * 343 */ 344 public void computeOffsetTransform() { 345 TempVars vars = TempVars.get(); 346 Matrix4f tmpMat = vars.tempMat42; 347 348 // Compute the cached world matrix 349 cachedOffsetMat.loadIdentity(); 350 cachedOffsetMat.setRotationQuaternion(prevBatchTransforms.getRotation()); 351 cachedOffsetMat.setTranslation(prevBatchTransforms.getTranslation()); 352 353 354 Matrix4f scaleMat = vars.tempMat4; 355 scaleMat.loadIdentity(); 356 scaleMat.scale(prevBatchTransforms.getScale()); 357 cachedOffsetMat.multLocal(scaleMat); 358 cachedOffsetMat.invertLocal(); 359 360 tmpMat.loadIdentity(); 361 tmpMat.setRotationQuaternion(batchNode.getTransforms(this).getRotation()); 362 tmpMat.setTranslation(batchNode.getTransforms(this).getTranslation()); 363 scaleMat.loadIdentity(); 364 scaleMat.scale(batchNode.getTransforms(this).getScale()); 365 tmpMat.multLocal(scaleMat); 366 367 tmpMat.mult(cachedOffsetMat, cachedOffsetMat); 368 369 vars.release(); 370 } 371 372 /** 373 * Indicate that the transform of this spatial has changed and that 374 * a refresh is required. 375 */ 376 @Override 377 protected void setTransformRefresh() { 378 refreshFlags |= RF_TRANSFORM; 379 setBoundRefresh(); 380 } 381 382 /** 383 * Recomputes the matrix returned by {@link Geometry#getWorldMatrix() }. 384 * This will require a localized transform update for this geometry. 385 */ 386 public void computeWorldMatrix() { 387 // Force a local update of the geometry's transform 388 checkDoTransformUpdate(); 389 390 // Compute the cached world matrix 391 cachedWorldMat.loadIdentity(); 392 cachedWorldMat.setRotationQuaternion(worldTransform.getRotation()); 393 cachedWorldMat.setTranslation(worldTransform.getTranslation()); 394 395 TempVars vars = TempVars.get(); 396 Matrix4f scaleMat = vars.tempMat4; 397 scaleMat.loadIdentity(); 398 scaleMat.scale(worldTransform.getScale()); 399 cachedWorldMat.multLocal(scaleMat); 400 vars.release(); 401 } 402 403 /** 404 * A {@link Matrix4f matrix} that transforms the {@link Geometry#getMesh() mesh} 405 * from model space to world space. This matrix is computed based on the 406 * {@link Geometry#getWorldTransform() world transform} of this geometry. 407 * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() } 408 * before using this method. 409 * 410 * @return Matrix to transform from local space to world space 411 */ 412 public Matrix4f getWorldMatrix() { 413 return cachedWorldMat; 414 } 415 416 /** 417 * Sets the model bound to use for this geometry. 418 * This alters the bound used on the mesh as well via 419 * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and 420 * forces the world bounding volume to be recomputed. 421 * 422 * @param modelBound The model bound to set 423 */ 424 @Override 425 public void setModelBound(BoundingVolume modelBound) { 426 this.worldBound = null; 427 mesh.setBound(modelBound); 428 setBoundRefresh(); 429 430 // NOTE: Calling updateModelBound() would cause the mesh 431 // to recompute the bound based on the geometry thus making 432 // this call useless! 433 //updateModelBound(); 434 } 435 436 public int collideWith(Collidable other, CollisionResults results) { 437 // Force bound to update 438 checkDoBoundUpdate(); 439 // Update transform, and compute cached world matrix 440 computeWorldMatrix(); 441 442 assert (refreshFlags & (RF_BOUND | RF_TRANSFORM)) == 0; 443 444 if (mesh != null) { 445 // NOTE: BIHTree in mesh already checks collision with the 446 // mesh's bound 447 int prevSize = results.size(); 448 int added = mesh.collideWith(other, cachedWorldMat, worldBound, results); 449 int newSize = results.size(); 450 for (int i = prevSize; i < newSize; i++) { 451 results.getCollisionDirect(i).setGeometry(this); 452 } 453 return added; 454 } 455 return 0; 456 } 457 458 @Override 459 public void depthFirstTraversal(SceneGraphVisitor visitor) { 460 visitor.visit(this); 461 } 462 463 @Override 464 protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) { 465 } 466 467 public boolean isBatched() { 468 return batchNode != null; 469 } 470 471 /** 472 * This version of clone is a shallow clone, in other words, the 473 * same mesh is referenced as the original geometry. 474 * Exception: if the mesh is marked as being a software 475 * animated mesh, (bind pose is set) then the positions 476 * and normals are deep copied. 477 */ 478 @Override 479 public Geometry clone(boolean cloneMaterial) { 480 Geometry geomClone = (Geometry) super.clone(cloneMaterial); 481 geomClone.cachedWorldMat = cachedWorldMat.clone(); 482 if (material != null) { 483 if (cloneMaterial) { 484 geomClone.material = material.clone(); 485 } else { 486 geomClone.material = material; 487 } 488 } 489 490 if (mesh != null && mesh.getBuffer(Type.BindPosePosition) != null) { 491 geomClone.mesh = mesh.cloneForAnim(); 492 } 493 494 return geomClone; 495 } 496 497 /** 498 * This version of clone is a shallow clone, in other words, the 499 * same mesh is referenced as the original geometry. 500 * Exception: if the mesh is marked as being a software 501 * animated mesh, (bind pose is set) then the positions 502 * and normals are deep copied. 503 */ 504 @Override 505 public Geometry clone() { 506 return clone(true); 507 } 508 509 /** 510 * Creates a deep clone of the geometry, 511 * this creates an identical copy of the mesh 512 * with the vertexbuffer data duplicated. 513 */ 514 @Override 515 public Spatial deepClone() { 516 Geometry geomClone = clone(true); 517 geomClone.mesh = mesh.deepClone(); 518 return geomClone; 519 } 520 521 @Override 522 public void write(JmeExporter ex) throws IOException { 523 super.write(ex); 524 OutputCapsule oc = ex.getCapsule(this); 525 oc.write(mesh, "mesh", null); 526 if (material != null) { 527 oc.write(material.getAssetName(), "materialName", null); 528 } 529 oc.write(material, "material", null); 530 oc.write(ignoreTransform, "ignoreTransform", false); 531 } 532 533 @Override 534 public void read(JmeImporter im) throws IOException { 535 super.read(im); 536 InputCapsule ic = im.getCapsule(this); 537 mesh = (Mesh) ic.readSavable("mesh", null); 538 539 material = null; 540 String matName = ic.readString("materialName", null); 541 if (matName != null) { 542 // Material name is set, 543 // Attempt to load material via J3M 544 try { 545 material = im.getAssetManager().loadMaterial(matName); 546 } catch (AssetNotFoundException ex) { 547 // Cannot find J3M file. 548 logger.log(Level.FINE, "Cannot locate {0} for geometry {1}", new Object[]{matName, key}); 549 } 550 } 551 // If material is NULL, try to load it from the geometry 552 if (material == null) { 553 material = (Material) ic.readSavable("material", null); 554 } 555 ignoreTransform = ic.readBoolean("ignoreTransform", false); 556 557 if (ic.getSavableVersion(Geometry.class) == 0){ 558 // Fix shared mesh (if set) 559 Mesh sharedMesh = getUserData(UserData.JME_SHAREDMESH); 560 if (sharedMesh != null){ 561 getMesh().extractVertexData(sharedMesh); 562 } 563 } 564 } 565 } 566