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}