1 /* 2 * Copyright (c) 2009-2012 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.export.*; 35 import com.jme3.material.Material; 36 import com.jme3.math.Matrix4f; 37 import com.jme3.math.Transform; 38 import com.jme3.math.Vector3f; 39 import com.jme3.scene.mesh.IndexBuffer; 40 import com.jme3.util.IntMap.Entry; 41 import com.jme3.util.TempVars; 42 import java.io.IOException; 43 import java.nio.Buffer; 44 import java.nio.FloatBuffer; 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 import java.util.logging.Level; 51 import java.util.logging.Logger; 52 53 /** 54 * BatchNode holds geometrie that are batched version of all geometries that are in its sub scenegraph. 55 * There is one geometry per different material in the sub tree. 56 * this geometries are directly attached to the node in the scene graph. 57 * usage is like any other node except you have to call the {@link #batch()} method once all geoms have been attached to the sub scene graph and theire material set 58 * (see todo more automagic for further enhancements) 59 * all the geometry that have been batched are set to {@link CullHint#Always} to not render them. 60 * the sub geometries can be transformed as usual their transforms are used to update the mesh of the geometryBatch. 61 * sub geoms can be removed but it may be slower than the normal spatial removing 62 * Sub geoms can be added after the batch() method has been called but won't be batched and will be rendered as normal geometries. 63 * To integrate them in the batch you have to call the batch() method again on the batchNode. 64 * 65 * TODO normal or tangents or both looks a bit weird 66 * TODO more automagic (batch when needed in the updateLigicalState) 67 * @author Nehon 68 */ 69 public class BatchNode extends Node implements Savable { 70 71 private static final Logger logger = Logger.getLogger(BatchNode.class.getName()); 72 /** 73 * the map of geometry holding the batched meshes 74 */ 75 protected Map<Material, Batch> batches = new HashMap<Material, Batch>(); 76 /** 77 * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer 78 */ 79 private float[] tmpFloat; 80 private float[] tmpFloatN; 81 private float[] tmpFloatT; 82 int maxVertCount = 0; 83 boolean useTangents = false; 84 boolean needsFullRebatch = true; 85 86 /** 87 * Construct a batchNode 88 */ 89 public BatchNode() { 90 super(); 91 } 92 93 public BatchNode(String name) { 94 super(name); 95 } 96 97 @Override 98 public void updateGeometricState() { 99 if ((refreshFlags & RF_LIGHTLIST) != 0) { 100 updateWorldLightList(); 101 } 102 103 if ((refreshFlags & RF_TRANSFORM) != 0) { 104 // combine with parent transforms- same for all spatial 105 // subclasses. 106 updateWorldTransforms(); 107 } 108 109 if (!children.isEmpty()) { 110 // the important part- make sure child geometric state is refreshed 111 // first before updating own world bound. This saves 112 // a round-trip later on. 113 // NOTE 9/19/09 114 // Although it does save a round trip, 115 116 for (Spatial child : children.getArray()) { 117 child.updateGeometricState(); 118 } 119 120 for (Batch batch : batches.values()) { 121 if (batch.needMeshUpdate) { 122 batch.geometry.getMesh().updateBound(); 123 batch.geometry.updateWorldBound(); 124 batch.needMeshUpdate = false; 125 126 } 127 } 128 129 130 } 131 132 if ((refreshFlags & RF_BOUND) != 0) { 133 updateWorldBound(); 134 } 135 136 assert refreshFlags == 0; 137 } 138 139 protected Transform getTransforms(Geometry geom) { 140 return geom.getWorldTransform(); 141 } 142 143 protected void updateSubBatch(Geometry bg) { 144 Batch batch = batches.get(bg.getMaterial()); 145 if (batch != null) { 146 Mesh mesh = batch.geometry.getMesh(); 147 148 VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position); 149 FloatBuffer posBuf = (FloatBuffer) pvb.getData(); 150 VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal); 151 FloatBuffer normBuf = (FloatBuffer) nvb.getData(); 152 153 if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) { 154 155 VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent); 156 FloatBuffer tanBuf = (FloatBuffer) tvb.getData(); 157 doTransformsTangents(posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), bg.cachedOffsetMat); 158 tvb.updateData(tanBuf); 159 } else { 160 doTransforms(posBuf, normBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), bg.cachedOffsetMat); 161 } 162 pvb.updateData(posBuf); 163 nvb.updateData(normBuf); 164 165 166 batch.needMeshUpdate = true; 167 } 168 } 169 170 /** 171 * Batch this batchNode 172 * every geometry of the sub scene graph of this node will be batched into a single mesh that will be rendered in one call 173 */ 174 public void batch() { 175 doBatch(); 176 //we set the batch geometries to ignore transforms to avoid transforms of parent nodes to be applied twice 177 for (Batch batch : batches.values()) { 178 batch.geometry.setIgnoreTransform(true); 179 } 180 } 181 182 protected void doBatch() { 183 Map<Material, List<Geometry>> matMap = new HashMap<Material, List<Geometry>>(); 184 maxVertCount = 0; 185 int nbGeoms = 0; 186 187 gatherGeomerties(matMap, this, needsFullRebatch); 188 if (needsFullRebatch) { 189 for (Batch batch : batches.values()) { 190 batch.geometry.removeFromParent(); 191 } 192 batches.clear(); 193 } 194 for (Map.Entry<Material, List<Geometry>> entry : matMap.entrySet()) { 195 Mesh m = new Mesh(); 196 Material material = entry.getKey(); 197 List<Geometry> list = entry.getValue(); 198 nbGeoms += list.size(); 199 if (!needsFullRebatch) { 200 list.add(batches.get(material).geometry); 201 } 202 mergeGeometries(m, list); 203 m.setDynamic(); 204 Batch batch = new Batch(); 205 206 batch.geometry = new Geometry(name + "-batch" + batches.size()); 207 batch.geometry.setMaterial(material); 208 this.attachChild(batch.geometry); 209 210 211 batch.geometry.setMesh(m); 212 batch.geometry.getMesh().updateCounts(); 213 batch.geometry.getMesh().updateBound(); 214 batches.put(material, batch); 215 } 216 217 logger.log(Level.INFO, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()}); 218 219 220 //init temp float arrays 221 tmpFloat = new float[maxVertCount * 3]; 222 tmpFloatN = new float[maxVertCount * 3]; 223 if (useTangents) { 224 tmpFloatT = new float[maxVertCount * 4]; 225 } 226 } 227 228 private void gatherGeomerties(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) { 229 230 if (n.getClass() == Geometry.class) { 231 232 if (!isBatch(n) && n.getBatchHint() != BatchHint.Never) { 233 Geometry g = (Geometry) n; 234 if (!g.isBatched() || rebatch) { 235 if (g.getMaterial() == null) { 236 throw new IllegalStateException("No material is set for Geometry: " + g.getName() + " please set a material before batching"); 237 } 238 List<Geometry> list = map.get(g.getMaterial()); 239 if (list == null) { 240 list = new ArrayList<Geometry>(); 241 map.put(g.getMaterial(), list); 242 } 243 g.setTransformRefresh(); 244 list.add(g); 245 } 246 } 247 248 } else if (n instanceof Node) { 249 for (Spatial child : ((Node) n).getChildren()) { 250 if (child instanceof BatchNode) { 251 continue; 252 } 253 gatherGeomerties(map, child, rebatch); 254 } 255 } 256 257 } 258 259 private boolean isBatch(Spatial s) { 260 for (Batch batch : batches.values()) { 261 if (batch.geometry == s) { 262 return true; 263 } 264 } 265 return false; 266 } 267 268 /** 269 * Sets the material to the all the batches of this BatchNode 270 * use setMaterial(Material material,int batchIndex) to set a material to a specific batch 271 * 272 * @param material the material to use for this geometry 273 */ 274 @Override 275 public void setMaterial(Material material) { 276 // for (Batch batch : batches.values()) { 277 // batch.geometry.setMaterial(material); 278 // } 279 throw new UnsupportedOperationException("Unsupported for now, please set the material on the geoms before batching"); 280 } 281 282 /** 283 * Returns the material that is used for the first batch of this BatchNode 284 * 285 * use getMaterial(Material material,int batchIndex) to get a material from a specific batch 286 * 287 * @return the material that is used for the first batch of this BatchNode 288 * 289 * @see #setMaterial(com.jme3.material.Material) 290 */ 291 public Material getMaterial() { 292 if (!batches.isEmpty()) { 293 Batch b = batches.get(batches.keySet().iterator().next()); 294 return b.geometry.getMaterial(); 295 } 296 return null;//material; 297 } 298 299 // /** 300 // * Sets the material to the a specific batch of this BatchNode 301 // * 302 // * 303 // * @param material the material to use for this geometry 304 // */ 305 // public void setMaterial(Material material,int batchIndex) { 306 // if (!batches.isEmpty()) { 307 // 308 // } 309 // 310 // } 311 // 312 // /** 313 // * Returns the material that is used for the first batch of this BatchNode 314 // * 315 // * use getMaterial(Material material,int batchIndex) to get a material from a specific batch 316 // * 317 // * @return the material that is used for the first batch of this BatchNode 318 // * 319 // * @see #setMaterial(com.jme3.material.Material) 320 // */ 321 // public Material getMaterial(int batchIndex) { 322 // if (!batches.isEmpty()) { 323 // Batch b = batches.get(batches.keySet().iterator().next()); 324 // return b.geometry.getMaterial(); 325 // } 326 // return null;//material; 327 // } 328 @Override 329 public void write(JmeExporter ex) throws IOException { 330 super.write(ex); 331 OutputCapsule oc = ex.getCapsule(this); 332 // 333 // if (material != null) { 334 // oc.write(material.getAssetName(), "materialName", null); 335 // } 336 // oc.write(material, "material", null); 337 338 } 339 340 @Override 341 public void read(JmeImporter im) throws IOException { 342 super.read(im); 343 InputCapsule ic = im.getCapsule(this); 344 345 346 // material = null; 347 // String matName = ic.readString("materialName", null); 348 // if (matName != null) { 349 // // Material name is set, 350 // // Attempt to load material via J3M 351 // try { 352 // material = im.getAssetManager().loadMaterial(matName); 353 // } catch (AssetNotFoundException ex) { 354 // // Cannot find J3M file. 355 // logger.log(Level.FINE, "Could not load J3M file {0} for Geometry.", 356 // matName); 357 // } 358 // } 359 // // If material is NULL, try to load it from the geometry 360 // if (material == null) { 361 // material = (Material) ic.readSavable("material", null); 362 // } 363 364 } 365 366 /** 367 * Merges all geometries in the collection into 368 * the output mesh. Does not take into account materials. 369 * 370 * @param geometries 371 * @param outMesh 372 */ 373 private void mergeGeometries(Mesh outMesh, List<Geometry> geometries) { 374 int[] compsForBuf = new int[VertexBuffer.Type.values().length]; 375 VertexBuffer.Format[] formatForBuf = new VertexBuffer.Format[compsForBuf.length]; 376 377 int totalVerts = 0; 378 int totalTris = 0; 379 int totalLodLevels = 0; 380 381 Mesh.Mode mode = null; 382 for (Geometry geom : geometries) { 383 totalVerts += geom.getVertexCount(); 384 totalTris += geom.getTriangleCount(); 385 totalLodLevels = Math.min(totalLodLevels, geom.getMesh().getNumLodLevels()); 386 if (maxVertCount < geom.getVertexCount()) { 387 maxVertCount = geom.getVertexCount(); 388 } 389 Mesh.Mode listMode; 390 int components; 391 switch (geom.getMesh().getMode()) { 392 case Points: 393 listMode = Mesh.Mode.Points; 394 components = 1; 395 break; 396 case LineLoop: 397 case LineStrip: 398 case Lines: 399 listMode = Mesh.Mode.Lines; 400 components = 2; 401 break; 402 case TriangleFan: 403 case TriangleStrip: 404 case Triangles: 405 listMode = Mesh.Mode.Triangles; 406 components = 3; 407 break; 408 default: 409 throw new UnsupportedOperationException(); 410 } 411 412 for (VertexBuffer vb : geom.getMesh().getBufferList().getArray()) { 413 compsForBuf[vb.getBufferType().ordinal()] = vb.getNumComponents(); 414 formatForBuf[vb.getBufferType().ordinal()] = vb.getFormat(); 415 } 416 417 if (mode != null && mode != listMode) { 418 throw new UnsupportedOperationException("Cannot combine different" 419 + " primitive types: " + mode + " != " + listMode); 420 } 421 mode = listMode; 422 compsForBuf[VertexBuffer.Type.Index.ordinal()] = components; 423 } 424 425 outMesh.setMode(mode); 426 if (totalVerts >= 65536) { 427 // make sure we create an UnsignedInt buffer so 428 // we can fit all of the meshes 429 formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedInt; 430 } else { 431 formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedShort; 432 } 433 434 // generate output buffers based on retrieved info 435 for (int i = 0; i < compsForBuf.length; i++) { 436 if (compsForBuf[i] == 0) { 437 continue; 438 } 439 440 Buffer data; 441 if (i == VertexBuffer.Type.Index.ordinal()) { 442 data = VertexBuffer.createBuffer(formatForBuf[i], compsForBuf[i], totalTris); 443 } else { 444 data = VertexBuffer.createBuffer(formatForBuf[i], compsForBuf[i], totalVerts); 445 } 446 447 VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.values()[i]); 448 vb.setupData(VertexBuffer.Usage.Dynamic, compsForBuf[i], formatForBuf[i], data); 449 outMesh.setBuffer(vb); 450 } 451 452 int globalVertIndex = 0; 453 int globalTriIndex = 0; 454 455 for (Geometry geom : geometries) { 456 Mesh inMesh = geom.getMesh(); 457 geom.batch(this, globalVertIndex); 458 459 int geomVertCount = inMesh.getVertexCount(); 460 int geomTriCount = inMesh.getTriangleCount(); 461 462 for (int bufType = 0; bufType < compsForBuf.length; bufType++) { 463 VertexBuffer inBuf = inMesh.getBuffer(VertexBuffer.Type.values()[bufType]); 464 465 VertexBuffer outBuf = outMesh.getBuffer(VertexBuffer.Type.values()[bufType]); 466 467 if (outBuf == null) { 468 continue; 469 } 470 471 if (VertexBuffer.Type.Index.ordinal() == bufType) { 472 int components = compsForBuf[bufType]; 473 474 IndexBuffer inIdx = inMesh.getIndicesAsList(); 475 IndexBuffer outIdx = outMesh.getIndexBuffer(); 476 477 for (int tri = 0; tri < geomTriCount; tri++) { 478 for (int comp = 0; comp < components; comp++) { 479 int idx = inIdx.get(tri * components + comp) + globalVertIndex; 480 outIdx.put((globalTriIndex + tri) * components + comp, idx); 481 } 482 } 483 } else if (VertexBuffer.Type.Position.ordinal() == bufType) { 484 FloatBuffer inPos = (FloatBuffer) inBuf.getData(); 485 FloatBuffer outPos = (FloatBuffer) outBuf.getData(); 486 doCopyBuffer(inPos, globalVertIndex, outPos, 3); 487 } else if (VertexBuffer.Type.Normal.ordinal() == bufType || VertexBuffer.Type.Tangent.ordinal() == bufType) { 488 FloatBuffer inPos = (FloatBuffer) inBuf.getData(); 489 FloatBuffer outPos = (FloatBuffer) outBuf.getData(); 490 doCopyBuffer(inPos, globalVertIndex, outPos, compsForBuf[bufType]); 491 if (VertexBuffer.Type.Tangent.ordinal() == bufType) { 492 useTangents = true; 493 } 494 } else { 495 inBuf.copyElements(0, outBuf, globalVertIndex, geomVertCount); 496 // for (int vert = 0; vert < geomVertCount; vert++) { 497 // int curGlobalVertIndex = globalVertIndex + vert; 498 // inBuf.copyElement(vert, outBuf, curGlobalVertIndex); 499 // } 500 } 501 } 502 503 globalVertIndex += geomVertCount; 504 globalTriIndex += geomTriCount; 505 } 506 } 507 508 private void doTransforms(FloatBuffer bufPos, FloatBuffer bufNorm, int start, int end, Matrix4f transform) { 509 TempVars vars = TempVars.get(); 510 Vector3f pos = vars.vect1; 511 Vector3f norm = vars.vect2; 512 513 int length = (end - start) * 3; 514 515 // offset is given in element units 516 // convert to be in component units 517 int offset = start * 3; 518 bufPos.position(offset); 519 bufNorm.position(offset); 520 bufPos.get(tmpFloat, 0, length); 521 bufNorm.get(tmpFloatN, 0, length); 522 int index = 0; 523 while (index < length) { 524 pos.x = tmpFloat[index]; 525 norm.x = tmpFloatN[index++]; 526 pos.y = tmpFloat[index]; 527 norm.y = tmpFloatN[index++]; 528 pos.z = tmpFloat[index]; 529 norm.z = tmpFloatN[index]; 530 531 transform.mult(pos, pos); 532 transform.multNormal(norm, norm); 533 534 index -= 2; 535 tmpFloat[index] = pos.x; 536 tmpFloatN[index++] = norm.x; 537 tmpFloat[index] = pos.y; 538 tmpFloatN[index++] = norm.y; 539 tmpFloat[index] = pos.z; 540 tmpFloatN[index++] = norm.z; 541 542 } 543 vars.release(); 544 bufPos.position(offset); 545 //using bulk put as it's faster 546 bufPos.put(tmpFloat, 0, length); 547 bufNorm.position(offset); 548 //using bulk put as it's faster 549 bufNorm.put(tmpFloatN, 0, length); 550 } 551 552 private void doTransformsTangents(FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) { 553 TempVars vars = TempVars.get(); 554 Vector3f pos = vars.vect1; 555 Vector3f norm = vars.vect2; 556 Vector3f tan = vars.vect3; 557 558 int length = (end - start) * 3; 559 int tanLength = (end - start) * 4; 560 561 // offset is given in element units 562 // convert to be in component units 563 int offset = start * 3; 564 int tanOffset = start * 4; 565 566 bufPos.position(offset); 567 bufNorm.position(offset); 568 bufTangents.position(tanOffset); 569 bufPos.get(tmpFloat, 0, length); 570 bufNorm.get(tmpFloatN, 0, length); 571 bufTangents.get(tmpFloatT, 0, tanLength); 572 573 int index = 0; 574 int tanIndex = 0; 575 while (index < length) { 576 pos.x = tmpFloat[index]; 577 norm.x = tmpFloatN[index++]; 578 pos.y = tmpFloat[index]; 579 norm.y = tmpFloatN[index++]; 580 pos.z = tmpFloat[index]; 581 norm.z = tmpFloatN[index]; 582 583 tan.x = tmpFloatT[tanIndex++]; 584 tan.y = tmpFloatT[tanIndex++]; 585 tan.z = tmpFloatT[tanIndex++]; 586 587 transform.mult(pos, pos); 588 transform.multNormal(norm, norm); 589 transform.multNormal(tan, tan); 590 591 index -= 2; 592 tanIndex -= 3; 593 594 tmpFloat[index] = pos.x; 595 tmpFloatN[index++] = norm.x; 596 tmpFloat[index] = pos.y; 597 tmpFloatN[index++] = norm.y; 598 tmpFloat[index] = pos.z; 599 tmpFloatN[index++] = norm.z; 600 601 tmpFloatT[tanIndex++] = tan.x; 602 tmpFloatT[tanIndex++] = tan.y; 603 tmpFloatT[tanIndex++] = tan.z; 604 605 //Skipping 4th element of tangent buffer (handedness) 606 tanIndex++; 607 608 } 609 vars.release(); 610 bufPos.position(offset); 611 //using bulk put as it's faster 612 bufPos.put(tmpFloat, 0, length); 613 bufNorm.position(offset); 614 //using bulk put as it's faster 615 bufNorm.put(tmpFloatN, 0, length); 616 bufTangents.position(tanOffset); 617 //using bulk put as it's faster 618 bufTangents.put(tmpFloatT, 0, tanLength); 619 } 620 621 private void doCopyBuffer(FloatBuffer inBuf, int offset, FloatBuffer outBuf, int componentSize) { 622 TempVars vars = TempVars.get(); 623 Vector3f pos = vars.vect1; 624 625 // offset is given in element units 626 // convert to be in component units 627 offset *= componentSize; 628 629 for (int i = 0; i < inBuf.capacity() / componentSize; i++) { 630 pos.x = inBuf.get(i * componentSize + 0); 631 pos.y = inBuf.get(i * componentSize + 1); 632 pos.z = inBuf.get(i * componentSize + 2); 633 634 outBuf.put(offset + i * componentSize + 0, pos.x); 635 outBuf.put(offset + i * componentSize + 1, pos.y); 636 outBuf.put(offset + i * componentSize + 2, pos.z); 637 } 638 vars.release(); 639 } 640 641 protected class Batch { 642 643 Geometry geometry; 644 boolean needMeshUpdate = false; 645 } 646 647 protected void setNeedsFullRebatch(boolean needsFullRebatch) { 648 this.needsFullRebatch = needsFullRebatch; 649 } 650 651 public int getOffsetIndex(Geometry batchedGeometry){ 652 return batchedGeometry.startIndex; 653 } 654 } 655