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 33 package com.jme3.scene; 34 35 import com.jme3.bounding.BoundingVolume; 36 import com.jme3.collision.Collidable; 37 import com.jme3.collision.CollisionResults; 38 import com.jme3.export.JmeExporter; 39 import com.jme3.export.JmeImporter; 40 import com.jme3.export.Savable; 41 import com.jme3.material.Material; 42 import com.jme3.util.SafeArrayList; 43 import java.io.IOException; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Queue; 47 import java.util.logging.Level; 48 import java.util.logging.Logger; 49 50 51 /** 52 * <code>Node</code> defines an internal node of a scene graph. The internal 53 * node maintains a collection of children and handles merging said children 54 * into a single bound to allow for very fast culling of multiple nodes. Node 55 * allows for any number of children to be attached. 56 * 57 * @author Mark Powell 58 * @author Gregg Patton 59 * @author Joshua Slack 60 */ 61 public class Node extends Spatial implements Savable { 62 63 private static final Logger logger = Logger.getLogger(Node.class.getName()); 64 65 66 /** 67 * This node's children. 68 */ 69 protected SafeArrayList<Spatial> children = new SafeArrayList<Spatial>(Spatial.class); 70 71 /** 72 * Serialization only. Do not use. 73 */ Node()74 public Node() { 75 } 76 77 /** 78 * Constructor instantiates a new <code>Node</code> with a default empty 79 * list for containing children. 80 * 81 * @param name 82 * the name of the scene element. This is required for 83 * identification and comparision purposes. 84 */ Node(String name)85 public Node(String name) { 86 super(name); 87 } 88 89 /** 90 * 91 * <code>getQuantity</code> returns the number of children this node 92 * maintains. 93 * 94 * @return the number of children this node maintains. 95 */ getQuantity()96 public int getQuantity() { 97 return children.size(); 98 } 99 100 @Override setTransformRefresh()101 protected void setTransformRefresh(){ 102 super.setTransformRefresh(); 103 for (Spatial child : children.getArray()){ 104 if ((child.refreshFlags & RF_TRANSFORM) != 0) 105 continue; 106 107 child.setTransformRefresh(); 108 } 109 } 110 111 @Override setLightListRefresh()112 protected void setLightListRefresh(){ 113 super.setLightListRefresh(); 114 for (Spatial child : children.getArray()){ 115 if ((child.refreshFlags & RF_LIGHTLIST) != 0) 116 continue; 117 118 child.setLightListRefresh(); 119 } 120 } 121 122 @Override updateWorldBound()123 protected void updateWorldBound(){ 124 super.updateWorldBound(); 125 126 // for a node, the world bound is a combination of all it's children 127 // bounds 128 BoundingVolume resultBound = null; 129 for (Spatial child : children.getArray()) { 130 // child bound is assumed to be updated 131 assert (child.refreshFlags & RF_BOUND) == 0; 132 if (resultBound != null) { 133 // merge current world bound with child world bound 134 resultBound.mergeLocal(child.getWorldBound()); 135 } else { 136 // set world bound to first non-null child world bound 137 if (child.getWorldBound() != null) { 138 resultBound = child.getWorldBound().clone(this.worldBound); 139 } 140 } 141 } 142 this.worldBound = resultBound; 143 } 144 145 @Override updateLogicalState(float tpf)146 public void updateLogicalState(float tpf){ 147 super.updateLogicalState(tpf); 148 149 if (children.isEmpty()) { 150 return; 151 } 152 153 for (Spatial child : children.getArray()) { 154 child.updateLogicalState(tpf); 155 } 156 } 157 158 @Override updateGeometricState()159 public void updateGeometricState(){ 160 if ((refreshFlags & RF_LIGHTLIST) != 0){ 161 updateWorldLightList(); 162 } 163 164 if ((refreshFlags & RF_TRANSFORM) != 0){ 165 // combine with parent transforms- same for all spatial 166 // subclasses. 167 updateWorldTransforms(); 168 } 169 170 if (!children.isEmpty()) { 171 // the important part- make sure child geometric state is refreshed 172 // first before updating own world bound. This saves 173 // a round-trip later on. 174 // NOTE 9/19/09 175 // Although it does save a round trip, 176 for (Spatial child : children.getArray()) { 177 child.updateGeometricState(); 178 } 179 } 180 181 if ((refreshFlags & RF_BOUND) != 0){ 182 updateWorldBound(); 183 } 184 185 assert refreshFlags == 0; 186 } 187 188 /** 189 * <code>getTriangleCount</code> returns the number of triangles contained 190 * in all sub-branches of this node that contain geometry. 191 * 192 * @return the triangle count of this branch. 193 */ 194 @Override getTriangleCount()195 public int getTriangleCount() { 196 int count = 0; 197 if(children != null) { 198 for(int i = 0; i < children.size(); i++) { 199 count += children.get(i).getTriangleCount(); 200 } 201 } 202 203 return count; 204 } 205 206 /** 207 * <code>getVertexCount</code> returns the number of vertices contained 208 * in all sub-branches of this node that contain geometry. 209 * 210 * @return the vertex count of this branch. 211 */ 212 @Override getVertexCount()213 public int getVertexCount() { 214 int count = 0; 215 if(children != null) { 216 for(int i = 0; i < children.size(); i++) { 217 count += children.get(i).getVertexCount(); 218 } 219 } 220 221 return count; 222 } 223 224 /** 225 * <code>attachChild</code> attaches a child to this node. This node 226 * becomes the child's parent. The current number of children maintained is 227 * returned. 228 * <br> 229 * If the child already had a parent it is detached from that former parent. 230 * 231 * @param child 232 * the child to attach to this node. 233 * @return the number of children maintained by this node. 234 * @throws NullPointerException If child is null. 235 */ attachChild(Spatial child)236 public int attachChild(Spatial child) { 237 if (child == null) 238 throw new NullPointerException(); 239 240 if (child.getParent() != this && child != this) { 241 if (child.getParent() != null) { 242 child.getParent().detachChild(child); 243 } 244 child.setParent(this); 245 children.add(child); 246 247 // XXX: Not entirely correct? Forces bound update up the 248 // tree stemming from the attached child. Also forces 249 // transform update down the tree- 250 child.setTransformRefresh(); 251 child.setLightListRefresh(); 252 if (logger.isLoggable(Level.INFO)) { 253 logger.log(Level.INFO,"Child ({0}) attached to this node ({1})", 254 new Object[]{child.getName(), getName()}); 255 } 256 } 257 258 return children.size(); 259 } 260 261 /** 262 * 263 * <code>attachChildAt</code> attaches a child to this node at an index. This node 264 * becomes the child's parent. The current number of children maintained is 265 * returned. 266 * <br> 267 * If the child already had a parent it is detached from that former parent. 268 * 269 * @param child 270 * the child to attach to this node. 271 * @return the number of children maintained by this node. 272 * @throws NullPointerException if child is null. 273 */ attachChildAt(Spatial child, int index)274 public int attachChildAt(Spatial child, int index) { 275 if (child == null) 276 throw new NullPointerException(); 277 278 if (child.getParent() != this && child != this) { 279 if (child.getParent() != null) { 280 child.getParent().detachChild(child); 281 } 282 child.setParent(this); 283 children.add(index, child); 284 child.setTransformRefresh(); 285 child.setLightListRefresh(); 286 if (logger.isLoggable(Level.INFO)) { 287 logger.log(Level.INFO,"Child ({0}) attached to this node ({1})", 288 new Object[]{child.getName(), getName()}); 289 } 290 } 291 292 return children.size(); 293 } 294 295 /** 296 * <code>detachChild</code> removes a given child from the node's list. 297 * This child will no longer be maintained. 298 * 299 * @param child 300 * the child to remove. 301 * @return the index the child was at. -1 if the child was not in the list. 302 */ detachChild(Spatial child)303 public int detachChild(Spatial child) { 304 if (child == null) 305 throw new NullPointerException(); 306 307 if (child.getParent() == this) { 308 int index = children.indexOf(child); 309 if (index != -1) { 310 detachChildAt(index); 311 } 312 return index; 313 } 314 315 return -1; 316 } 317 318 /** 319 * <code>detachChild</code> removes a given child from the node's list. 320 * This child will no longe be maintained. Only the first child with a 321 * matching name is removed. 322 * 323 * @param childName 324 * the child to remove. 325 * @return the index the child was at. -1 if the child was not in the list. 326 */ detachChildNamed(String childName)327 public int detachChildNamed(String childName) { 328 if (childName == null) 329 throw new NullPointerException(); 330 331 for (int x = 0, max = children.size(); x < max; x++) { 332 Spatial child = children.get(x); 333 if (childName.equals(child.getName())) { 334 detachChildAt( x ); 335 return x; 336 } 337 } 338 return -1; 339 } 340 341 /** 342 * 343 * <code>detachChildAt</code> removes a child at a given index. That child 344 * is returned for saving purposes. 345 * 346 * @param index 347 * the index of the child to be removed. 348 * @return the child at the supplied index. 349 */ detachChildAt(int index)350 public Spatial detachChildAt(int index) { 351 Spatial child = children.remove(index); 352 if ( child != null ) { 353 child.setParent( null ); 354 logger.log(Level.INFO, "{0}: Child removed.", this.toString()); 355 356 // since a child with a bound was detached; 357 // our own bound will probably change. 358 setBoundRefresh(); 359 360 // our world transform no longer influences the child. 361 // XXX: Not neccessary? Since child will have transform updated 362 // when attached anyway. 363 child.setTransformRefresh(); 364 // lights are also inherited from parent 365 child.setLightListRefresh(); 366 } 367 return child; 368 } 369 370 /** 371 * 372 * <code>detachAllChildren</code> removes all children attached to this 373 * node. 374 */ detachAllChildren()375 public void detachAllChildren() { 376 for ( int i = children.size() - 1; i >= 0; i-- ) { 377 detachChildAt(i); 378 } 379 logger.log(Level.INFO, "{0}: All children removed.", this.toString()); 380 } 381 382 /** 383 * <code>getChildIndex</code> returns the index of the given spatial 384 * in this node's list of children. 385 * @param sp 386 * The spatial to look up 387 * @return 388 * The index of the spatial in the node's children, or -1 389 * if the spatial is not attached to this node 390 */ getChildIndex(Spatial sp)391 public int getChildIndex(Spatial sp) { 392 return children.indexOf(sp); 393 } 394 395 /** 396 * More efficient than e.g detaching and attaching as no updates are needed. 397 * 398 * @param index1 The index of the first child to swap 399 * @param index2 The index of the second child to swap 400 */ swapChildren(int index1, int index2)401 public void swapChildren(int index1, int index2) { 402 Spatial c2 = children.get(index2); 403 Spatial c1 = children.remove(index1); 404 children.add(index1, c2); 405 children.remove(index2); 406 children.add(index2, c1); 407 } 408 409 /** 410 * 411 * <code>getChild</code> returns a child at a given index. 412 * 413 * @param i 414 * the index to retrieve the child from. 415 * @return the child at a specified index. 416 */ getChild(int i)417 public Spatial getChild(int i) { 418 return children.get(i); 419 } 420 421 /** 422 * <code>getChild</code> returns the first child found with exactly the 423 * given name (case sensitive.) 424 * 425 * @param name 426 * the name of the child to retrieve. If null, we'll return null. 427 * @return the child if found, or null. 428 */ getChild(String name)429 public Spatial getChild(String name) { 430 if (name == null) 431 return null; 432 433 for (Spatial child : children.getArray()) { 434 if (name.equals(child.getName())) { 435 return child; 436 } else if(child instanceof Node) { 437 Spatial out = ((Node)child).getChild(name); 438 if(out != null) { 439 return out; 440 } 441 } 442 } 443 return null; 444 } 445 446 /** 447 * determines if the provided Spatial is contained in the children list of 448 * this node. 449 * 450 * @param spat 451 * the child object to look for. 452 * @return true if the object is contained, false otherwise. 453 */ hasChild(Spatial spat)454 public boolean hasChild(Spatial spat) { 455 if (children.contains(spat)) 456 return true; 457 458 for (Spatial child : children.getArray()) { 459 if (child instanceof Node && ((Node) child).hasChild(spat)) 460 return true; 461 } 462 463 return false; 464 } 465 466 /** 467 * Returns all children to this node. Note that modifying that given 468 * list is not allowed. 469 * 470 * @return a list containing all children to this node 471 */ getChildren()472 public List<Spatial> getChildren() { 473 return children; 474 } 475 476 @Override setMaterial(Material mat)477 public void setMaterial(Material mat){ 478 for (int i = 0; i < children.size(); i++){ 479 children.get(i).setMaterial(mat); 480 } 481 } 482 483 @Override setLodLevel(int lod)484 public void setLodLevel(int lod){ 485 super.setLodLevel(lod); 486 for (Spatial child : children.getArray()) { 487 child.setLodLevel(lod); 488 } 489 } 490 collideWith(Collidable other, CollisionResults results)491 public int collideWith(Collidable other, CollisionResults results){ 492 int total = 0; 493 for (Spatial child : children.getArray()){ 494 total += child.collideWith(other, results); 495 } 496 return total; 497 } 498 499 500 /** 501 * Returns flat list of Spatials implementing the specified class AND 502 * with name matching the specified pattern. 503 * </P> <P> 504 * Note that we are <i>matching</i> the pattern, therefore the pattern 505 * must match the entire pattern (i.e. it behaves as if it is sandwiched 506 * between "^" and "$"). 507 * You can set regex modes, like case insensitivity, by using the (?X) 508 * or (?X:Y) constructs. 509 * </P> <P> 510 * By design, it is always safe to code loops like:<CODE><PRE> 511 * for (Spatial spatial : node.descendantMatches(AClass.class, "regex")) 512 * </PRE></CODE> 513 * </P> <P> 514 * "Descendants" does not include self, per the definition of the word. 515 * To test for descendants AND self, you must do a 516 * <code>node.matches(aClass, aRegex)</code> + 517 * <code>node.descendantMatches(aClass, aRegex)</code>. 518 * <P> 519 * 520 * @param spatialSubclass Subclass which matching Spatials must implement. 521 * Null causes all Spatials to qualify. 522 * @param nameRegex Regular expression to match Spatial name against. 523 * Null causes all Names to qualify. 524 * @return Non-null, but possibly 0-element, list of matching Spatials (also Instances extending Spatials). 525 * 526 * @see java.util.regex.Pattern 527 * @see Spatial#matches(java.lang.Class, java.lang.String) 528 */ 529 @SuppressWarnings("unchecked") descendantMatches( Class<T> spatialSubclass, String nameRegex)530 public <T extends Spatial>List<T> descendantMatches( 531 Class<T> spatialSubclass, String nameRegex) { 532 List<T> newList = new ArrayList<T>(); 533 if (getQuantity() < 1) return newList; 534 for (Spatial child : getChildren()) { 535 if (child.matches(spatialSubclass, nameRegex)) 536 newList.add((T)child); 537 if (child instanceof Node) 538 newList.addAll(((Node) child).descendantMatches( 539 spatialSubclass, nameRegex)); 540 } 541 return newList; 542 } 543 544 /** 545 * Convenience wrapper. 546 * 547 * @see #descendantMatches(java.lang.Class, java.lang.String) 548 */ descendantMatches( Class<T> spatialSubclass)549 public <T extends Spatial>List<T> descendantMatches( 550 Class<T> spatialSubclass) { 551 return descendantMatches(spatialSubclass, null); 552 } 553 554 /** 555 * Convenience wrapper. 556 * 557 * @see #descendantMatches(java.lang.Class, java.lang.String) 558 */ descendantMatches(String nameRegex)559 public <T extends Spatial>List<T> descendantMatches(String nameRegex) { 560 return descendantMatches(null, nameRegex); 561 } 562 563 @Override clone(boolean cloneMaterials)564 public Node clone(boolean cloneMaterials){ 565 Node nodeClone = (Node) super.clone(cloneMaterials); 566 // nodeClone.children = new ArrayList<Spatial>(); 567 // for (Spatial child : children){ 568 // Spatial childClone = child.clone(); 569 // childClone.parent = nodeClone; 570 // nodeClone.children.add(childClone); 571 // } 572 return nodeClone; 573 } 574 575 @Override deepClone()576 public Spatial deepClone(){ 577 Node nodeClone = (Node) super.clone(); 578 nodeClone.children = new SafeArrayList<Spatial>(Spatial.class); 579 for (Spatial child : children){ 580 Spatial childClone = child.deepClone(); 581 childClone.parent = nodeClone; 582 nodeClone.children.add(childClone); 583 } 584 return nodeClone; 585 } 586 587 @Override write(JmeExporter e)588 public void write(JmeExporter e) throws IOException { 589 super.write(e); 590 e.getCapsule(this).writeSavableArrayList(new ArrayList(children), "children", null); 591 } 592 593 @Override read(JmeImporter e)594 public void read(JmeImporter e) throws IOException { 595 // XXX: Load children before loading itself!! 596 // This prevents empty children list if controls query 597 // it in Control.setSpatial(). 598 599 children = new SafeArrayList( Spatial.class, 600 e.getCapsule(this).readSavableArrayList("children", null) ); 601 602 // go through children and set parent to this node 603 if (children != null) { 604 for (Spatial child : children.getArray()) { 605 child.parent = this; 606 } 607 } 608 609 super.read(e); 610 } 611 612 @Override setModelBound(BoundingVolume modelBound)613 public void setModelBound(BoundingVolume modelBound) { 614 if(children != null) { 615 for (Spatial child : children.getArray()) { 616 child.setModelBound(modelBound != null ? modelBound.clone(null) : null); 617 } 618 } 619 } 620 621 @Override updateModelBound()622 public void updateModelBound() { 623 if(children != null) { 624 for (Spatial child : children.getArray()) { 625 child.updateModelBound(); 626 } 627 } 628 } 629 630 @Override depthFirstTraversal(SceneGraphVisitor visitor)631 public void depthFirstTraversal(SceneGraphVisitor visitor) { 632 for (Spatial child : children.getArray()) { 633 child.depthFirstTraversal(visitor); 634 } 635 visitor.visit(this); 636 } 637 638 @Override breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue)639 protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) { 640 queue.addAll(children); 641 } 642 643 } 644