001/** 002 * Copyright (c) 2008-2014 Ardor Labs, Inc. 003 * 004 * This file is part of Ardor3D. 005 * 006 * Ardor3D is free software: you can redistribute it and/or modify it 007 * under the terms of its license which may be found in the accompanying 008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>. 009 */ 010 011package com.ardor3d.util.geom; 012 013import java.nio.FloatBuffer; 014import java.nio.IntBuffer; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.Collection; 018import java.util.EnumMap; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022 023import com.ardor3d.bounding.BoundingVolume; 024import com.ardor3d.renderer.IndexMode; 025import com.ardor3d.renderer.state.RenderState; 026import com.ardor3d.renderer.state.RenderState.StateType; 027import com.ardor3d.scenegraph.FloatBufferData; 028import com.ardor3d.scenegraph.IndexBufferData; 029import com.ardor3d.scenegraph.Mesh; 030import com.ardor3d.scenegraph.MeshData; 031import com.ardor3d.scenegraph.Node; 032import com.ardor3d.scenegraph.Spatial; 033import com.ardor3d.scenegraph.visitor.Visitor; 034 035/** 036 * Utility for combining multiple Meshes into a single Mesh. Note that you generally will want to combine Mesh objects 037 * that have the same render states. 038 * 039 * XXX: should add in a way to combine only meshes with similar renderstates<br> 040 * XXX: Might be able to reduce memory usage in the singular case where all sources do not have indices defined 041 * (arrays).<br> 042 * XXX: combining of triangle strips may not work properly? <br> 043 * XXX: Could be smarter about texcoords and have a tuple size per channel.<br> 044 */ 045public class MeshCombiner { 046 public static final float[] DEFAULT_COLOR = { 1f, 1f, 1f, 1f }; 047 public static final float[] DEFAULT_NORMAL = { 0f, 1f, 0f }; 048 public static final float[] DEFAULT_TEXCOORD = { 0 }; 049 050 /** 051 * <p> 052 * Combine all mesh objects that fall under the scene graph the given source node. All Mesh objects must have 053 * vertices and texcoords that have the same tuple width. It is possible to merge Mesh objects together that have 054 * mismatched normals/colors/etc. (eg. one with colors and one without.) 055 * </p> 056 * 057 * @param source 058 * our source node 059 * @return the combined Mesh. 060 */ 061 public final static Mesh combine(final Node source) { 062 return combine(source, new MeshCombineLogic()); 063 } 064 065 public final static Mesh combine(final Spatial source, final MeshCombineLogic logic) { 066 final List<Mesh> sources = new ArrayList<>(); 067 source.acceptVisitor(new Visitor() { 068 @Override 069 public void visit(final Spatial spatial) { 070 if (spatial instanceof Mesh) { 071 sources.add((Mesh) spatial); 072 } 073 } 074 }, true); 075 076 return combine(sources, logic); 077 } 078 079 /** 080 * Combine the given array of Mesh objects into a single Mesh. All Mesh objects must have vertices and texcoords 081 * that have the same tuple width. It is possible to merge Mesh objects together that have mismatched 082 * normals/colors/etc. (eg. one with colors and one without.) 083 * 084 * @param sources 085 * our Mesh objects to combine. 086 * @return the combined Mesh. 087 */ 088 public final static Mesh combine(final Mesh... sources) { 089 return combine(new ArrayList<>(Arrays.asList(sources))); 090 } 091 092 /** 093 * Combine the given collection of Mesh objects into a single Mesh. All Mesh objects must have vertices and 094 * texcoords that have the same tuple width. It is possible to merge Mesh objects together that have mismatched 095 * normals/colors/etc. (eg. one with colors and one without.) 096 * 097 * @param sources 098 * our collection of Mesh objects to combine. 099 * @return the combined Mesh. 100 */ 101 public final static Mesh combine(final Collection<Mesh> sources) { 102 return combine(sources, new MeshCombineLogic()); 103 } 104 105 public final static Mesh combine(final Collection<Mesh> sources, final MeshCombineLogic logic) { 106 if (sources == null || sources.isEmpty()) { 107 return null; 108 } 109 110 // go through each MeshData to see what buffers we need and validate sizes. 111 for (final Mesh mesh : sources) { 112 logic.addSource(mesh); 113 } 114 115 // initialize return buffers 116 logic.initDataBuffers(); 117 118 // combine sources into buffers 119 logic.combineSources(); 120 121 // get and return our combined mesh 122 return logic.getCombinedMesh(); 123 } 124 125 public static class MeshCombineLogic { 126 protected boolean useIndices = false, useNormals = false, useTextures = false, useColors = false, first = true; 127 protected int maxTextures = 0, totalVertices = 0, totalIndices = 0, texCoords = 2, vertCoords = 3; 128 protected IndexMode mode = null; 129 protected EnumMap<StateType, RenderState> states = null; 130 protected MeshData data = new MeshData(); 131 protected BoundingVolume volumeType = null; 132 protected List<Mesh> sources = new ArrayList<>(); 133 private FloatBufferData vertices; 134 private FloatBufferData colors; 135 private FloatBufferData normals; 136 private List<FloatBufferData> texCoordsList; 137 138 public Mesh getMesh() { 139 final Mesh mesh = new Mesh("combined"); 140 mesh.setMeshData(data); 141 return mesh; 142 } 143 144 public Mesh getCombinedMesh() { 145 final Mesh mesh = getMesh(); 146 147 // set our bounding volume using the volume type of our first source found above. 148 mesh.setModelBound(volumeType); 149 150 // set the render states from the first mesh 151 for (final RenderState state : states.values()) { 152 mesh.setRenderState(state); 153 } 154 155 return mesh; 156 } 157 158 public void combineSources() { 159 final IndexCombiner iCombiner = new IndexCombiner(); 160 161 // Walk through our source meshes and populate return MeshData buffers. 162 int vertexOffset = 0; 163 for (final Mesh mesh : sources) { 164 165 final MeshData md = mesh.getMeshData(); 166 167 // Vertices 168 md.getVertexBuffer().rewind(); 169 vertices.getBuffer().put(mesh.getWorldVectors(null)); 170 171 // Normals 172 if (useNormals) { 173 final FloatBuffer nb = md.getNormalBuffer(); 174 if (nb != null) { 175 nb.rewind(); 176 normals.getBuffer().put(mesh.getWorldNormals(null)); 177 } else { 178 for (int i = 0; i < md.getVertexCount(); i++) { 179 normals.getBuffer().put(DEFAULT_NORMAL); 180 } 181 } 182 } 183 184 // Colors 185 if (useColors) { 186 final FloatBuffer cb = md.getColorBuffer(); 187 if (cb != null) { 188 cb.rewind(); 189 colors.getBuffer().put(cb); 190 } else { 191 for (int i = 0; i < md.getVertexCount(); i++) { 192 colors.getBuffer().put(DEFAULT_COLOR); 193 } 194 } 195 } 196 197 // Tex Coords 198 if (useTextures) { 199 for (int i = 0; i < maxTextures; i++) { 200 final FloatBuffer dest = texCoordsList.get(i).getBuffer(); 201 final FloatBuffer tb = md.getTextureBuffer(i); 202 if (tb != null) { 203 tb.rewind(); 204 dest.put(tb); 205 } else { 206 for (int j = 0; j < md.getVertexCount() * texCoords; j++) { 207 dest.put(DEFAULT_TEXCOORD); 208 } 209 } 210 } 211 } 212 213 // Indices 214 if (useIndices) { 215 iCombiner.addEntry(md, vertexOffset); 216 vertexOffset += md.getVertexCount(); 217 } 218 } 219 220 // Apply our index combiner to the mesh 221 if (useIndices) { 222 iCombiner.saveTo(data); 223 } else { 224 data.setIndexLengths(null); 225 data.setIndexMode(mode); 226 } 227 } 228 229 public void initDataBuffers() { 230 // Generate our buffers based on the information collected above and populate MeshData 231 vertices = new FloatBufferData(totalVertices * vertCoords, vertCoords); 232 data.setVertexCoords(vertices); 233 234 colors = useColors ? new FloatBufferData(totalVertices * 4, 4) : null; 235 data.setColorCoords(colors); 236 237 normals = useNormals ? new FloatBufferData(totalVertices * 3, 3) : null; 238 data.setNormalCoords(normals); 239 240 texCoordsList = new ArrayList<>(maxTextures); 241 for (int i = 0; i < maxTextures; i++) { 242 texCoordsList.add(new FloatBufferData(totalVertices * texCoords, texCoords)); 243 } 244 data.setTextureCoords(useTextures ? texCoordsList : null); 245 } 246 247 public void addSource(final Mesh mesh) { 248 sources.add(mesh); 249 250 // update world transforms 251 mesh.updateWorldTransform(false); 252 253 final MeshData md = mesh.getMeshData(); 254 if (first) { 255 // copy info from first mesh 256 vertCoords = md.getVertexCoords().getValuesPerTuple(); 257 volumeType = mesh.getModelBound(null); 258 states = mesh.getLocalRenderStates(); 259 first = false; 260 } else if (vertCoords != md.getVertexCoords().getValuesPerTuple()) { 261 throw new IllegalArgumentException("all MeshData vertex coords must use same tuple size."); 262 } 263 264 // update total vertices 265 totalVertices += md.getVertexCount(); 266 267 // check for indices 268 if (useIndices || md.getIndices() != null) { 269 useIndices = true; 270 if (md.getIndices() != null) { 271 totalIndices += md.getIndices().capacity(); 272 } else { 273 totalIndices += md.getVertexCount(); 274 } 275 } else { 276 mode = md.getIndexMode(0); 277 } 278 279 // check for normals 280 if (!useNormals && md.getNormalBuffer() != null) { 281 useNormals = true; 282 } 283 284 // check for colors 285 if (!useColors && md.getColorBuffer() != null) { 286 useColors = true; 287 } 288 289 // check for texcoord usage 290 if (md.getNumberOfUnits() > 0) { 291 if (!useTextures) { 292 useTextures = true; 293 texCoords = md.getTextureCoords(0).getValuesPerTuple(); 294 } else if (md.getTextureCoords(0) != null && texCoords != md.getTextureCoords(0).getValuesPerTuple()) { 295 throw new IllegalArgumentException("all MeshData objects with texcoords must use same tuple size."); 296 } 297 maxTextures = Math.max(maxTextures, md.getNumberOfUnits()); 298 } 299 } 300 } 301} 302 303class IndexCombiner { 304 Map<IndexMode, List<int[]>> sectionMap = new HashMap<>(); 305 306 public void addEntry(final MeshData source, final int vertexOffset) { 307 // arrays or elements? 308 if (source.getIndices() == null) { 309 // arrays... 310 int offset = 0; 311 int indexModeCounter = 0; 312 final IndexMode[] modes = source.getIndexModes(); 313 // walk through each section 314 for (int i = 0, maxI = source.getSectionCount(); i < maxI; i++) { 315 // make an int array and populate it. 316 final int size = source.getIndexLengths() != null ? source.getIndexLengths()[i] 317 : source.getVertexCount(); 318 final int[] indices = new int[size]; 319 for (int j = 0; j < size; j++) { 320 indices[j] = j + vertexOffset + offset; 321 } 322 323 // add to map 324 sectionMap.compute(modes[indexModeCounter], 325 (final IndexMode currentKey, final List<int[]> oldValue) -> { 326 final List<int[]> newValue; 327 if (oldValue == null) { 328 newValue = new ArrayList<>(); 329 } else { 330 newValue = oldValue; 331 } 332 newValue.add(indices); 333 return newValue; 334 }); 335 336 // move our offsets forward to the section 337 offset += size; 338 if (indexModeCounter < modes.length - 1) { 339 indexModeCounter++; 340 } 341 } 342 } else { 343 // elements... 344 final IndexBufferData<?> ib = source.getIndices(); 345 ib.rewind(); 346 int offset = 0; 347 int indexModeCounter = 0; 348 final IndexMode[] modes = source.getIndexModes(); 349 // walk through each section 350 for (int i = 0, maxI = source.getSectionCount(); i < maxI; i++) { 351 // make an int array and populate it. 352 final int size = source.getIndexLengths() != null ? source.getIndexLengths()[i] 353 : source.getIndices().capacity(); 354 final int[] indices = new int[size]; 355 for (int j = 0; j < size; j++) { 356 indices[j] = ib.get(j + offset) + vertexOffset; 357 } 358 359 // add to map 360 sectionMap.compute(modes[indexModeCounter], 361 (final IndexMode currentKey, final List<int[]> oldValue) -> { 362 final List<int[]> newValue; 363 if (oldValue == null) { 364 newValue = new ArrayList<>(); 365 } else { 366 newValue = oldValue; 367 } 368 newValue.add(indices); 369 return newValue; 370 }); 371 372 // move our offsets forward to the section 373 offset += size; 374 if (indexModeCounter < modes.length - 1) { 375 indexModeCounter++; 376 } 377 } 378 } 379 } 380 381 public void saveTo(final MeshData data) { 382 final List<IntBuffer> sections = new ArrayList<>(); 383 final List<IndexMode> modes = new ArrayList<>(); 384 int max = 0; 385 // walk through index modes and combine those we can. 386 for (final Map.Entry<IndexMode, List<int[]>> sectionMapEntry : sectionMap.entrySet()) { 387 final IndexMode mode = sectionMapEntry.getKey(); 388 final Collection<int[]> sources = sectionMapEntry.getValue(); 389 switch (mode) { 390 case Triangles: 391 case Quads: 392 case Lines: 393 case Points: { 394 // we can combine these as-is to our heart's content. 395 int size = 0; 396 for (final int[] indices : sources) { 397 size += indices.length; 398 } 399 max += size; 400 final IntBuffer newSection = BufferUtils.createIntBufferOnHeap(size); 401 for (final int[] indices : sources) { 402 newSection.put(indices); 403 } 404 // save 405 sections.add(newSection); 406 modes.add(mode); 407 break; 408 } 409 case TriangleFan: 410 case QuadStrip: 411 case LineLoop: 412 case LineStrip: { 413 // these have to be kept, as is. 414 int size; 415 for (final int[] indices : sources) { 416 size = indices.length; 417 max += size; 418 final IntBuffer newSection = BufferUtils.createIntBufferOnHeap(size); 419 newSection.put(indices); 420 421 sections.add(newSection); 422 modes.add(mode); 423 } 424 break; 425 } 426 case TriangleStrip: { 427 // we CAN combine these, but we have to add degenerate triangles. 428 int size = 0; 429 for (final int[] indices : sources) { 430 size += indices.length + 2; 431 } 432 size -= 2; 433 max += size; 434 final IntBuffer newSection = BufferUtils.createIntBufferOnHeap(size); 435 int i = 0; 436 for (final int[] indices : sources) { 437 if (i != 0) { 438 newSection.put(indices[0]); 439 } 440 newSection.put(indices); 441 if (i < sources.size() - 1) { 442 newSection.put(indices[indices.length - 1]); 443 } 444 i++; 445 } 446 // save 447 sections.add(newSection); 448 modes.add(mode); 449 break; 450 } 451 } 452 } 453 454 // compile into data 455 final IndexBufferData<?> finalIndices = BufferUtils.createIndexBufferData(max, data.getVertexCount() - 1); 456 data.setIndices(finalIndices); 457 final int[] sectionCounts = new int[sections.size()]; 458 for (int i = 0; i < sectionCounts.length; i++) { 459 final IntBuffer ib = sections.get(i); 460 ib.rewind(); 461 sectionCounts[i] = ib.remaining(); 462 while (ib.hasRemaining()) { 463 finalIndices.put(ib.get()); 464 } 465 } 466 467 data.setIndexLengths(sectionCounts); 468 data.setIndexModes(modes.toArray(new IndexMode[modes.size()])); 469 } 470}