All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jme3.scene.BatchNode Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2009-2021 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 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;

import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.math.Matrix4f;
import com.jme3.math.Vector3f;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.util.SafeArrayList;
import com.jme3.util.TempVars;
import com.jme3.util.clone.Cloner;
import com.jme3.util.clone.JmeCloneable;

/**
 * BatchNode holds geometries that are a batched version of all the geometries that are in its sub scenegraph.
 * There is one geometry per different material in the sub tree.
 * The 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 the geometries have been attached to the sub scene graph and their material set
 * (see todo more automagic for further enhancements)
 * All the geometries that have been batched are set to not be rendered - {@link Spatial.CullHint} is left intact.
 * 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 just be rendered as normal geometries.
 * To integrate them in the batch you have to call the batch() method again on the batchNode.
 * 

* TODO more automagic (batch when needed in the updateLogicalState) * * @author Nehon */ public class BatchNode extends GeometryGroupNode { private static final Logger logger = Logger.getLogger(BatchNode.class.getName()); /** * the list of geometry holding the batched meshes */ protected SafeArrayList batches = new SafeArrayList<>(Batch.class); /** * a map for storing the batches by geometry to quickly access the batch when updating */ protected Map batchesByGeom = new HashMap<>(); /** * 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 onTransformChange(Geometry geom) { updateSubBatch(geom); } @Override public void onMaterialChange(Geometry geom) { throw new UnsupportedOperationException( "Cannot set the material of a batched geometry, " + "change the material of the parent BatchNode."); } @Override public void onMeshChange(Geometry geom) { throw new UnsupportedOperationException( "Cannot set the mesh of a batched geometry"); } @Override public void onGeometryUnassociated(Geometry geom) { setNeedsFullRebatch(true); } protected Matrix4f getTransformMatrix(Geometry g) { return g.cachedWorldMat; } protected void updateSubBatch(Geometry bg) { Batch batch = batchesByGeom.get(bg); if (batch != null) { Mesh mesh = batch.geometry.getMesh(); Mesh origMesh = bg.getMesh(); VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position); VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal); VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent); VertexBuffer opvb = origMesh.getBuffer(VertexBuffer.Type.Position); VertexBuffer onvb = origMesh.getBuffer(VertexBuffer.Type.Normal); VertexBuffer otvb = origMesh.getBuffer(VertexBuffer.Type.Tangent); FloatBuffer posBuf = getFloatBuffer(pvb); FloatBuffer normBuf = getFloatBuffer(nvb); FloatBuffer tanBuf = getFloatBuffer(tvb); FloatBuffer oposBuf = getFloatBuffer(opvb); FloatBuffer onormBuf = getFloatBuffer(onvb); FloatBuffer otanBuf = getFloatBuffer(otvb); Matrix4f transformMat = getTransformMatrix(bg); doTransforms(oposBuf, onormBuf, otanBuf, posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat); pvb.updateData(posBuf); if (nvb != null) { nvb.updateData(normBuf); } if (tvb != null) { tvb.updateData(tanBuf); } batch.geometry.updateModelBound(); } } private FloatBuffer getFloatBuffer(VertexBuffer vb) { if (vb == null) { return null; } return (FloatBuffer) vb.getData(); } /** * 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.getArray()) { batch.geometry.setIgnoreTransform(true); batch.geometry.setUserData(UserData.JME_PHYSICSIGNORE, true); } } protected void doBatch() { Map> matMap = new HashMap<>(); int nbGeoms = 0; gatherGeometries(matMap, this, needsFullRebatch); if (needsFullRebatch) { for (Batch batch : batches.getArray()) { batch.geometry.removeFromParent(); } batches.clear(); batchesByGeom.clear(); } //only reset maxVertCount if there is something new to batch if (matMap.size() > 0) { maxVertCount = 0; } for (Map.Entry> entry : matMap.entrySet()) { Mesh m = new Mesh(); Material material = entry.getKey(); List list = entry.getValue(); nbGeoms += list.size(); String batchName = name + "-batch" + batches.size(); Batch batch; if (!needsFullRebatch) { batch = findBatchByMaterial(material); if (batch != null) { list.add(0, batch.geometry); batchName = batch.geometry.getName(); batch.geometry.removeFromParent(); } else { batch = new Batch(); } } else { batch = new Batch(); } mergeGeometries(m, list); m.setDynamic(); batch.updateGeomList(list); batch.geometry = new Geometry(batchName); batch.geometry.setMaterial(material); this.attachChild(batch.geometry); batch.geometry.setMesh(m); batch.geometry.getMesh().updateCounts(); batch.geometry.updateModelBound(); batches.add(batch); } if (batches.size() > 0) { needsFullRebatch = false; } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()}); } //init the temp arrays if something has been batched only. if (matMap.size() > 0) { //TODO these arrays should be allocated by chunk instead to avoid recreating them each time the batch is changed. //init temp float arrays tmpFloat = new float[maxVertCount * 3]; tmpFloatN = new float[maxVertCount * 3]; if (useTangents) { tmpFloatT = new float[maxVertCount * 4]; } } } //in case the detached spatial is a node, we unbatch all geometries in its subgraph @Override public Spatial detachChildAt(int index) { Spatial s = super.detachChildAt(index); if (s instanceof Node) { unbatchSubGraph(s); } return s; } /** * recursively visit the subgraph and unbatch geometries * * @param s */ private void unbatchSubGraph(Spatial s) { if (s instanceof Node) { for (Spatial sp : ((Node) s).getChildren()) { unbatchSubGraph(sp); } } else if (s instanceof Geometry) { Geometry g = (Geometry) s; if (g.isGrouped()) { g.unassociateFromGroupNode(); } } } private void gatherGeometries(Map> map, Spatial n, boolean rebatch) { if (n instanceof Geometry) { if (!isBatch(n) && n.getBatchHint() != BatchHint.Never) { Geometry g = (Geometry) n; if (!g.isGrouped() || rebatch) { if (g.getMaterial() == null) { throw new IllegalStateException("No material is set for Geometry: " + g.getName() + " please set a material before batching"); } List list = map.get(g.getMaterial()); if (list == null) { //trying to compare materials with the isEqual method for (Map.Entry> mat : map.entrySet()) { if (g.getMaterial().contentEquals(mat.getKey())) { list = mat.getValue(); } } } if (list == null) { list = new ArrayList(); 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; } gatherGeometries(map, child, rebatch); } } } private Batch findBatchByMaterial(Material m) { for (Batch batch : batches.getArray()) { if (batch.geometry.getMaterial().contentEquals(m)) { return batch; } } return null; } public final boolean isBatch(Spatial s) { for (Batch batch : batches.getArray()) { 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) { 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.iterator().next(); return b.geometry.getMaterial(); } return 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 geometries) { int[] compsForBuf = new int[VertexBuffer.Type.values().length]; VertexBuffer.Format[] formatForBuf = new VertexBuffer.Format[compsForBuf.length]; boolean[] normForBuf = new boolean[VertexBuffer.Type.values().length]; int totalVerts = 0; int totalTris = 0; int totalLodLevels = 0; int maxWeights = -1; Mesh.Mode mode = null; float lineWidth = 1f; 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; //float listLineWidth = 1f; 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; //listLineWidth = geom.getMesh().getLineWidth(); components = 2; break; case TriangleFan: case TriangleStrip: case Triangles: listMode = Mesh.Mode.Triangles; components = 3; break; default: throw new UnsupportedOperationException(); } for (VertexBuffer vb : geom.getMesh().getBufferList().getArray()) { int currentCompsForBuf = compsForBuf[vb.getBufferType().ordinal()]; if (vb.getBufferType() != VertexBuffer.Type.Index && currentCompsForBuf != 0 && currentCompsForBuf != vb.getNumComponents()) { throw new UnsupportedOperationException("The geometry " + geom + " buffer " + vb.getBufferType() + " has different number of components than the rest of the meshes " + "(this: " + vb.getNumComponents() + ", expected: " + currentCompsForBuf + ")"); } compsForBuf[vb.getBufferType().ordinal()] = vb.getNumComponents(); formatForBuf[vb.getBufferType().ordinal()] = vb.getFormat(); normForBuf[vb.getBufferType().ordinal()] = vb.isNormalized(); } maxWeights = Math.max(maxWeights, geom.getMesh().getMaxNumWeights()); if (mode != null && mode != listMode) { throw new UnsupportedOperationException("Cannot combine different" + " primitive types: " + mode + " != " + listMode); } mode = listMode; compsForBuf[VertexBuffer.Type.Index.ordinal()] = components; } outMesh.setMaxNumWeights(maxWeights); outMesh.setMode(mode); //outMesh.setLineWidth(lineWidth); 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); vb.setNormalized(normForBuf[i]); outMesh.setBuffer(vb); } int globalVertIndex = 0; int globalTriIndex = 0; for (Geometry geom : geometries) { Mesh inMesh = geom.getMesh(); if (!isBatch(geom)) { geom.associateWithGroupNode(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 { if (inBuf == null) { throw new IllegalArgumentException("Geometry " + geom.getName() + " has no " + outBuf.getBufferType() + " buffer whereas other geoms have. all geometries should have the same types of buffers.\n Try to use GeometryBatchFactory.alignBuffer() on the BatchNode before batching"); } else if (outBuf == null) { throw new IllegalArgumentException("Geometry " + geom.getName() + " has a " + outBuf.getBufferType() + " buffer whereas other geoms don't. all geometries should have the same types of buffers.\n Try to use GeometryBatchFactory.alignBuffer() on the BatchNode before batching"); } else { inBuf.copyElements(0, outBuf, globalVertIndex, geomVertCount); } } } globalVertIndex += geomVertCount; globalTriIndex += geomTriCount; } } private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bindBufTangents, 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; bindBufPos.rewind(); bindBufPos.get(tmpFloat, 0, length); if (bindBufNorm != null) { bindBufNorm.rewind(); bindBufNorm.get(tmpFloatN, 0, length); } if (bindBufTangents != null) { bindBufTangents.rewind(); bindBufTangents.get(tmpFloatT, 0, tanLength); } int index = 0; int tanIndex = 0; int index1, index2, tanIndex1, tanIndex2; while (index < length) { index1 = index + 1; index2 = index + 2; pos.x = tmpFloat[index]; pos.y = tmpFloat[index1]; pos.z = tmpFloat[index2]; transform.mult(pos, pos); tmpFloat[index] = pos.x; tmpFloat[index1] = pos.y; tmpFloat[index2] = pos.z; if (bindBufNorm != null) { norm.x = tmpFloatN[index]; norm.y = tmpFloatN[index1]; norm.z = tmpFloatN[index2]; transform.multNormal(norm, norm); tmpFloatN[index] = norm.x; tmpFloatN[index1] = norm.y; tmpFloatN[index2] = norm.z; } index += 3; if (bindBufTangents != null) { tanIndex1 = tanIndex + 1; tanIndex2 = tanIndex + 2; tan.x = tmpFloatT[tanIndex]; tan.y = tmpFloatT[tanIndex1]; tan.z = tmpFloatT[tanIndex2]; transform.multNormal(tan, tan); tmpFloatT[tanIndex] = tan.x; tmpFloatT[tanIndex1] = tan.y; tmpFloatT[tanIndex2] = tan.z; tanIndex += 4; } } vars.release(); //using bulk put as it's faster bufPos.position(offset); bufPos.put(tmpFloat, 0, length); if (bindBufNorm != null) { bufNorm.position(offset); bufNorm.put(tmpFloatN, 0, length); } if (bindBufTangents != null) { bufTangents.position(tanOffset); 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.limit() / componentSize; i++) { pos.x = inBuf.get(i * componentSize); pos.y = inBuf.get(i * componentSize + 1); pos.z = inBuf.get(i * componentSize + 2); outBuf.put(offset + i * componentSize, pos.x); outBuf.put(offset + i * componentSize + 1, pos.y); outBuf.put(offset + i * componentSize + 2, pos.z); } vars.release(); } protected class Batch implements JmeCloneable { /** * update the batchesByGeom map for this batch with the given List of geometries * * @param list */ void updateGeomList(List list) { for (Geometry geom : list) { if (!isBatch(geom)) { batchesByGeom.put(geom, this); } } } Geometry geometry; public final Geometry getGeometry() { return geometry; } @Override public Batch jmeClone() { try { return (Batch) super.clone(); } catch (CloneNotSupportedException ex) { throw new AssertionError(); } } @Override public void cloneFields(Cloner cloner, Object original) { this.geometry = cloner.clone(geometry); } } protected void setNeedsFullRebatch(boolean needsFullRebatch) { this.needsFullRebatch = needsFullRebatch; } @Override public Node clone(boolean cloneMaterials) { BatchNode clone = (BatchNode) super.clone(cloneMaterials); if (batches.size() > 0) { for (Batch b : batches) { for (int i = 0; i < clone.children.size(); i++) { if (clone.children.get(i).getName().equals(b.geometry.getName())) { clone.children.remove(i); break; } } } clone.needsFullRebatch = true; clone.batches = new SafeArrayList<>(Batch.class); clone.batchesByGeom = new HashMap(); clone.batch(); } return clone; } /** * Called internally by com.jme3.util.clone.Cloner. Do not call directly. */ @Override public void cloneFields(Cloner cloner, Object original) { super.cloneFields(cloner, original); this.batches = cloner.clone(batches); this.tmpFloat = cloner.clone(tmpFloat); this.tmpFloatN = cloner.clone(tmpFloatN); this.tmpFloatT = cloner.clone(tmpFloatT); HashMap newBatchesByGeom = new HashMap<>(); for (Map.Entry e : batchesByGeom.entrySet()) { newBatchesByGeom.put(cloner.clone(e.getKey()), cloner.clone(e.getValue())); } this.batchesByGeom = newBatchesByGeom; } @Override public int collideWith(Collidable other, CollisionResults results) { int total = 0; for (Spatial child : children.getArray()) { if (!isBatch(child)) { total += child.collideWith(other, results); } } return total; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy