diff options
Diffstat (limited to 'engine/src/core/com/jme3/scene/BatchNode.java')
-rw-r--r-- | engine/src/core/com/jme3/scene/BatchNode.java | 653 |
1 files changed, 653 insertions, 0 deletions
diff --git a/engine/src/core/com/jme3/scene/BatchNode.java b/engine/src/core/com/jme3/scene/BatchNode.java new file mode 100644 index 0000000..bc1b2cb --- /dev/null +++ b/engine/src/core/com/jme3/scene/BatchNode.java @@ -0,0 +1,653 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.scene; + +import com.jme3.export.*; +import com.jme3.material.Material; +import com.jme3.math.Matrix4f; +import com.jme3.math.Transform; +import com.jme3.math.Vector3f; +import com.jme3.scene.mesh.IndexBuffer; +import com.jme3.util.IntMap.Entry; +import com.jme3.util.TempVars; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * BatchNode holds geometrie that are batched version of all geometries that are in its sub scenegraph. + * There is one geometry per different material in the sub tree. + * this geometries are directly attached to the node in the scene graph. + * 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 + * (see todo more automagic for further enhancements) + * all the geometry that have been batched are set to {@link CullHint#Always} to not render them. + * the sub geometries can be transformed as usual their transforms are used to update the mesh of the geometryBatch. + * sub geoms can be removed but it may be slower than the normal spatial removing + * Sub geoms can be added after the batch() method has been called but won't be batched and will be rendered as normal geometries. + * To integrate them in the batch you have to call the batch() method again on the batchNode. + * + * TODO normal or tangents or both looks a bit weird + * TODO more automagic (batch when needed in the updateLigicalState) + * @author Nehon + */ +public class BatchNode extends Node implements Savable { + + private static final Logger logger = Logger.getLogger(BatchNode.class.getName()); + /** + * the map of geometry holding the batched meshes + */ + protected Map<Material, Batch> batches = new HashMap<Material, Batch>(); + /** + * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer + */ + private float[] tmpFloat; + private float[] tmpFloatN; + private float[] tmpFloatT; + int maxVertCount = 0; + boolean useTangents = false; + boolean needsFullRebatch = true; + + /** + * Construct a batchNode + */ + public BatchNode() { + super(); + } + + public BatchNode(String name) { + super(name); + } + + @Override + public void updateGeometricState() { + if ((refreshFlags & RF_LIGHTLIST) != 0) { + updateWorldLightList(); + } + + if ((refreshFlags & RF_TRANSFORM) != 0) { + // combine with parent transforms- same for all spatial + // subclasses. + updateWorldTransforms(); + } + + if (!children.isEmpty()) { + // the important part- make sure child geometric state is refreshed + // first before updating own world bound. This saves + // a round-trip later on. + // NOTE 9/19/09 + // Although it does save a round trip, + + for (Spatial child : children.getArray()) { + child.updateGeometricState(); + } + + for (Batch batch : batches.values()) { + if (batch.needMeshUpdate) { + batch.geometry.getMesh().updateBound(); + batch.geometry.updateWorldBound(); + batch.needMeshUpdate = false; + + } + } + + + } + + if ((refreshFlags & RF_BOUND) != 0) { + updateWorldBound(); + } + + assert refreshFlags == 0; + } + + protected Transform getTransforms(Geometry geom) { + return geom.getWorldTransform(); + } + + protected void updateSubBatch(Geometry bg) { + Batch batch = batches.get(bg.getMaterial()); + if (batch != null) { + Mesh mesh = batch.geometry.getMesh(); + + VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position); + FloatBuffer posBuf = (FloatBuffer) pvb.getData(); + VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal); + FloatBuffer normBuf = (FloatBuffer) nvb.getData(); + + if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) { + + VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent); + FloatBuffer tanBuf = (FloatBuffer) tvb.getData(); + doTransformsTangents(posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), bg.cachedOffsetMat); + tvb.updateData(tanBuf); + } else { + doTransforms(posBuf, normBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), bg.cachedOffsetMat); + } + pvb.updateData(posBuf); + nvb.updateData(normBuf); + + + batch.needMeshUpdate = true; + } + } + + /** + * Batch this batchNode + * every geometry of the sub scene graph of this node will be batched into a single mesh that will be rendered in one call + */ + public void batch() { + doBatch(); + //we set the batch geometries to ignore transforms to avoid transforms of parent nodes to be applied twice + for (Batch batch : batches.values()) { + batch.geometry.setIgnoreTransform(true); + } + } + + protected void doBatch() { + Map<Material, List<Geometry>> matMap = new HashMap<Material, List<Geometry>>(); + maxVertCount = 0; + int nbGeoms = 0; + + gatherGeomerties(matMap, this, needsFullRebatch); + if (needsFullRebatch) { + for (Batch batch : batches.values()) { + batch.geometry.removeFromParent(); + } + batches.clear(); + } + + for (Material material : matMap.keySet()) { + Mesh m = new Mesh(); + List<Geometry> list = matMap.get(material); + nbGeoms += list.size(); + if (!needsFullRebatch) { + list.add(batches.get(material).geometry); + } + mergeGeometries(m, list); + m.setDynamic(); + Batch batch = new Batch(); + + batch.geometry = new Geometry(name + "-batch" + batches.size()); + batch.geometry.setMaterial(material); + this.attachChild(batch.geometry); + + + batch.geometry.setMesh(m); + batch.geometry.getMesh().updateCounts(); + batch.geometry.getMesh().updateBound(); + batches.put(material, batch); + } + + logger.log(Level.INFO, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()}); + + + //init temp float arrays + tmpFloat = new float[maxVertCount * 3]; + tmpFloatN = new float[maxVertCount * 3]; + if (useTangents) { + tmpFloatT = new float[maxVertCount * 4]; + } + } + + private void gatherGeomerties(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) { + + if (n.getClass() == Geometry.class) { + + if (!isBatch(n) && n.getBatchHint() != BatchHint.Never) { + Geometry g = (Geometry) n; + if (!g.isBatched() || rebatch) { + if (g.getMaterial() == null) { + throw new IllegalStateException("No material is set for Geometry: " + g.getName() + " please set a material before batching"); + } + List<Geometry> list = map.get(g.getMaterial()); + if (list == null) { + list = new ArrayList<Geometry>(); + map.put(g.getMaterial(), list); + } + g.setTransformRefresh(); + list.add(g); + } + } + + } else if (n instanceof Node) { + for (Spatial child : ((Node) n).getChildren()) { + if (child instanceof BatchNode) { + continue; + } + gatherGeomerties(map, child, rebatch); + } + } + + } + + private boolean isBatch(Spatial s) { + for (Batch batch : batches.values()) { + if (batch.geometry == s) { + return true; + } + } + return false; + } + + /** + * Sets the material to the all the batches of this BatchNode + * use setMaterial(Material material,int batchIndex) to set a material to a specific batch + * + * @param material the material to use for this geometry + */ + @Override + public void setMaterial(Material material) { +// for (Batch batch : batches.values()) { +// batch.geometry.setMaterial(material); +// } + throw new UnsupportedOperationException("Unsupported for now, please set the material on the geoms before batching"); + } + + /** + * Returns the material that is used for the first batch of this BatchNode + * + * use getMaterial(Material material,int batchIndex) to get a material from a specific batch + * + * @return the material that is used for the first batch of this BatchNode + * + * @see #setMaterial(com.jme3.material.Material) + */ + public Material getMaterial() { + if (!batches.isEmpty()) { + Batch b = batches.get(batches.keySet().iterator().next()); + return b.geometry.getMaterial(); + } + return null;//material; + } + +// /** +// * Sets the material to the a specific batch of this BatchNode +// * +// * +// * @param material the material to use for this geometry +// */ +// public void setMaterial(Material material,int batchIndex) { +// if (!batches.isEmpty()) { +// +// } +// +// } +// +// /** +// * Returns the material that is used for the first batch of this BatchNode +// * +// * use getMaterial(Material material,int batchIndex) to get a material from a specific batch +// * +// * @return the material that is used for the first batch of this BatchNode +// * +// * @see #setMaterial(com.jme3.material.Material) +// */ +// public Material getMaterial(int batchIndex) { +// if (!batches.isEmpty()) { +// Batch b = batches.get(batches.keySet().iterator().next()); +// return b.geometry.getMaterial(); +// } +// return null;//material; +// } + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); +// +// if (material != null) { +// oc.write(material.getAssetName(), "materialName", null); +// } +// oc.write(material, "material", null); + + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + + +// material = null; +// String matName = ic.readString("materialName", null); +// if (matName != null) { +// // Material name is set, +// // Attempt to load material via J3M +// try { +// material = im.getAssetManager().loadMaterial(matName); +// } catch (AssetNotFoundException ex) { +// // Cannot find J3M file. +// logger.log(Level.FINE, "Could not load J3M file {0} for Geometry.", +// matName); +// } +// } +// // If material is NULL, try to load it from the geometry +// if (material == null) { +// material = (Material) ic.readSavable("material", null); +// } + + } + + /** + * Merges all geometries in the collection into + * the output mesh. Does not take into account materials. + * + * @param geometries + * @param outMesh + */ + private void mergeGeometries(Mesh outMesh, List<Geometry> geometries) { + int[] compsForBuf = new int[VertexBuffer.Type.values().length]; + VertexBuffer.Format[] formatForBuf = new VertexBuffer.Format[compsForBuf.length]; + + int totalVerts = 0; + int totalTris = 0; + int totalLodLevels = 0; + + Mesh.Mode mode = null; + for (Geometry geom : geometries) { + totalVerts += geom.getVertexCount(); + totalTris += geom.getTriangleCount(); + totalLodLevels = Math.min(totalLodLevels, geom.getMesh().getNumLodLevels()); + if (maxVertCount < geom.getVertexCount()) { + maxVertCount = geom.getVertexCount(); + } + Mesh.Mode listMode; + int components; + switch (geom.getMesh().getMode()) { + case Points: + listMode = Mesh.Mode.Points; + components = 1; + break; + case LineLoop: + case LineStrip: + case Lines: + listMode = Mesh.Mode.Lines; + components = 2; + break; + case TriangleFan: + case TriangleStrip: + case Triangles: + listMode = Mesh.Mode.Triangles; + components = 3; + break; + default: + throw new UnsupportedOperationException(); + } + + for (Entry<VertexBuffer> entry : geom.getMesh().getBuffers()) { + compsForBuf[entry.getKey()] = entry.getValue().getNumComponents(); + formatForBuf[entry.getKey()] = entry.getValue().getFormat(); + } + + if (mode != null && mode != listMode) { + throw new UnsupportedOperationException("Cannot combine different" + + " primitive types: " + mode + " != " + listMode); + } + mode = listMode; + compsForBuf[VertexBuffer.Type.Index.ordinal()] = components; + } + + outMesh.setMode(mode); + if (totalVerts >= 65536) { + // make sure we create an UnsignedInt buffer so + // we can fit all of the meshes + formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedInt; + } else { + formatForBuf[VertexBuffer.Type.Index.ordinal()] = VertexBuffer.Format.UnsignedShort; + } + + // generate output buffers based on retrieved info + for (int i = 0; i < compsForBuf.length; i++) { + if (compsForBuf[i] == 0) { + continue; + } + + Buffer data; + if (i == VertexBuffer.Type.Index.ordinal()) { + data = VertexBuffer.createBuffer(formatForBuf[i], compsForBuf[i], totalTris); + } else { + data = VertexBuffer.createBuffer(formatForBuf[i], compsForBuf[i], totalVerts); + } + + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.values()[i]); + vb.setupData(VertexBuffer.Usage.Dynamic, compsForBuf[i], formatForBuf[i], data); + outMesh.setBuffer(vb); + } + + int globalVertIndex = 0; + int globalTriIndex = 0; + + for (Geometry geom : geometries) { + Mesh inMesh = geom.getMesh(); + geom.batch(this, globalVertIndex); + + int geomVertCount = inMesh.getVertexCount(); + int geomTriCount = inMesh.getTriangleCount(); + + for (int bufType = 0; bufType < compsForBuf.length; bufType++) { + VertexBuffer inBuf = inMesh.getBuffer(VertexBuffer.Type.values()[bufType]); + + VertexBuffer outBuf = outMesh.getBuffer(VertexBuffer.Type.values()[bufType]); + + if (outBuf == null) { + continue; + } + + if (VertexBuffer.Type.Index.ordinal() == bufType) { + int components = compsForBuf[bufType]; + + IndexBuffer inIdx = inMesh.getIndicesAsList(); + IndexBuffer outIdx = outMesh.getIndexBuffer(); + + for (int tri = 0; tri < geomTriCount; tri++) { + for (int comp = 0; comp < components; comp++) { + int idx = inIdx.get(tri * components + comp) + globalVertIndex; + outIdx.put((globalTriIndex + tri) * components + comp, idx); + } + } + } else if (VertexBuffer.Type.Position.ordinal() == bufType) { + FloatBuffer inPos = (FloatBuffer) inBuf.getData(); + FloatBuffer outPos = (FloatBuffer) outBuf.getData(); + doCopyBuffer(inPos, globalVertIndex, outPos, 3); + } else if (VertexBuffer.Type.Normal.ordinal() == bufType || VertexBuffer.Type.Tangent.ordinal() == bufType) { + FloatBuffer inPos = (FloatBuffer) inBuf.getData(); + FloatBuffer outPos = (FloatBuffer) outBuf.getData(); + doCopyBuffer(inPos, globalVertIndex, outPos, compsForBuf[bufType]); + if (VertexBuffer.Type.Tangent.ordinal() == bufType) { + useTangents = true; + } + } else { + inBuf.copyElements(0, outBuf, globalVertIndex, geomVertCount); +// for (int vert = 0; vert < geomVertCount; vert++) { +// int curGlobalVertIndex = globalVertIndex + vert; +// inBuf.copyElement(vert, outBuf, curGlobalVertIndex); +// } + } + } + + globalVertIndex += geomVertCount; + globalTriIndex += geomTriCount; + } + } + + private void doTransforms(FloatBuffer bufPos, FloatBuffer bufNorm, int start, int end, Matrix4f transform) { + TempVars vars = TempVars.get(); + Vector3f pos = vars.vect1; + Vector3f norm = vars.vect2; + + int length = (end - start) * 3; + + // offset is given in element units + // convert to be in component units + int offset = start * 3; + bufPos.position(offset); + bufNorm.position(offset); + bufPos.get(tmpFloat, 0, length); + bufNorm.get(tmpFloatN, 0, length); + int index = 0; + while (index < length) { + pos.x = tmpFloat[index]; + norm.x = tmpFloatN[index++]; + pos.y = tmpFloat[index]; + norm.y = tmpFloatN[index++]; + pos.z = tmpFloat[index]; + norm.z = tmpFloatN[index]; + + transform.mult(pos, pos); + transform.multNormal(norm, norm); + + index -= 2; + tmpFloat[index] = pos.x; + tmpFloatN[index++] = norm.x; + tmpFloat[index] = pos.y; + tmpFloatN[index++] = norm.y; + tmpFloat[index] = pos.z; + tmpFloatN[index++] = norm.z; + + } + vars.release(); + bufPos.position(offset); + //using bulk put as it's faster + bufPos.put(tmpFloat, 0, length); + bufNorm.position(offset); + //using bulk put as it's faster + bufNorm.put(tmpFloatN, 0, length); + } + + private void doTransformsTangents(FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) { + TempVars vars = TempVars.get(); + Vector3f pos = vars.vect1; + Vector3f norm = vars.vect2; + Vector3f tan = vars.vect3; + + int length = (end - start) * 3; + int tanLength = (end - start) * 4; + + // offset is given in element units + // convert to be in component units + int offset = start * 3; + int tanOffset = start * 4; + + bufPos.position(offset); + bufNorm.position(offset); + bufTangents.position(tanOffset); + bufPos.get(tmpFloat, 0, length); + bufNorm.get(tmpFloatN, 0, length); + bufTangents.get(tmpFloatT, 0, tanLength); + + int index = 0; + int tanIndex = 0; + while (index < length) { + pos.x = tmpFloat[index]; + norm.x = tmpFloatN[index++]; + pos.y = tmpFloat[index]; + norm.y = tmpFloatN[index++]; + pos.z = tmpFloat[index]; + norm.z = tmpFloatN[index]; + + tan.x = tmpFloatT[tanIndex++]; + tan.y = tmpFloatT[tanIndex++]; + tan.z = tmpFloatT[tanIndex++]; + + transform.mult(pos, pos); + transform.multNormal(norm, norm); + transform.multNormal(tan, tan); + + index -= 2; + tanIndex -= 3; + + tmpFloat[index] = pos.x; + tmpFloatN[index++] = norm.x; + tmpFloat[index] = pos.y; + tmpFloatN[index++] = norm.y; + tmpFloat[index] = pos.z; + tmpFloatN[index++] = norm.z; + + tmpFloatT[tanIndex++] = tan.x; + tmpFloatT[tanIndex++] = tan.y; + tmpFloatT[tanIndex++] = tan.z; + + //Skipping 4th element of tangent buffer (handedness) + tanIndex++; + + } + vars.release(); + bufPos.position(offset); + //using bulk put as it's faster + bufPos.put(tmpFloat, 0, length); + bufNorm.position(offset); + //using bulk put as it's faster + bufNorm.put(tmpFloatN, 0, length); + bufTangents.position(tanOffset); + //using bulk put as it's faster + bufTangents.put(tmpFloatT, 0, tanLength); + } + + private void doCopyBuffer(FloatBuffer inBuf, int offset, FloatBuffer outBuf, int componentSize) { + TempVars vars = TempVars.get(); + Vector3f pos = vars.vect1; + + // offset is given in element units + // convert to be in component units + offset *= componentSize; + + for (int i = 0; i < inBuf.capacity() / componentSize; i++) { + pos.x = inBuf.get(i * componentSize + 0); + pos.y = inBuf.get(i * componentSize + 1); + pos.z = inBuf.get(i * componentSize + 2); + + outBuf.put(offset + i * componentSize + 0, pos.x); + outBuf.put(offset + i * componentSize + 1, pos.y); + outBuf.put(offset + i * componentSize + 2, pos.z); + } + vars.release(); + } + + protected class Batch { + + Geometry geometry; + boolean needMeshUpdate = false; + } + + protected void setNeedsFullRebatch(boolean needsFullRebatch) { + this.needsFullRebatch = needsFullRebatch; + } + + public int getOffsetIndex(Geometry batchedGeometry){ + return batchedGeometry.startIndex; + } +} |