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 */
32package com.jme3.scene;
33
34import com.jme3.export.*;
35import com.jme3.material.Material;
36import com.jme3.math.Matrix4f;
37import com.jme3.math.Transform;
38import com.jme3.math.Vector3f;
39import com.jme3.scene.mesh.IndexBuffer;
40import com.jme3.util.IntMap.Entry;
41import com.jme3.util.TempVars;
42import java.io.IOException;
43import java.nio.Buffer;
44import java.nio.FloatBuffer;
45import java.util.ArrayList;
46import java.util.HashMap;
47import java.util.List;
48import java.util.Map;
49import java.util.Set;
50import java.util.logging.Level;
51import 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 */
69public 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