/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.badlogic.gdx.graphics.g3d; import java.util.Comparator; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.VertexAttributes; import com.badlogic.gdx.graphics.g3d.model.MeshPart; import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder; import com.badlogic.gdx.graphics.g3d.utils.RenderableSorter; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.FlushablePool; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.Pool; /** ModelCache tries to combine multiple render calls into a single render call by merging them where possible. Can be used for * multiple type of models (e.g. varying vertex attributes or materials), the ModelCache will combine where possible. Can be used * dynamically (e.g. every frame) or statically (e.g. to combine part of scenery). Be aware that any combined vertices are * directly transformed, therefore the resulting {@link Renderable#worldTransform} might not be suitable for sorting anymore (such * as the default sorter of ModelBatch does). * @author Xoppa */ public class ModelCache implements Disposable, RenderableProvider { /** Allows to reuse one or more meshes while avoiding creating new objects. Depending on the implementation it might add memory * optimizations as well. Call the {@link #obtain(VertexAttributes, int, int)} method to obtain a mesh which can at minimum the * specified amount of vertices and indices. Call the {@link #flush()} method to flush the pool ant release all previously * obtained meshes. */ public interface MeshPool extends Disposable { /** Will try to reuse or, when not possible to reuse, optionally create a {@link Mesh} that meets the specified criteria. * @param vertexAttributes the vertex attributes of the mesh to obtain * @param vertexCount the minimum amount vertices the mesh should be able to store * @param indexCount the minimum amount of indices the mesh should be able to store * @return the obtained Mesh, or null when no mesh could be obtained. */ Mesh obtain (VertexAttributes vertexAttributes, int vertexCount, int indexCount); /** Releases all previously obtained {@link Mesh}es using the the {@link #obtain(VertexAttributes, int, int)} method. */ void flush (); } /** A basic {@link MeshPool} implementation that avoids creating new meshes at the cost of memory usage. It does this by making * the mesh always the maximum (32k) size. Use this when for dynamic caching where you need to obtain meshes very frequently * (typically every frame). * @author Xoppa */ public static class SimpleMeshPool implements MeshPool { // FIXME Make a better (preferable JNI) MeshPool implementation private Array<Mesh> freeMeshes = new Array<Mesh>(); private Array<Mesh> usedMeshes = new Array<Mesh>(); @Override public void flush () { freeMeshes.addAll(usedMeshes); usedMeshes.clear(); } @Override public Mesh obtain (VertexAttributes vertexAttributes, int vertexCount, int indexCount) { for (int i = 0, n = freeMeshes.size; i < n; ++i) { final Mesh mesh = freeMeshes.get(i); if (mesh.getVertexAttributes().equals(vertexAttributes) && mesh.getMaxVertices() >= vertexCount && mesh.getMaxIndices() >= indexCount) { freeMeshes.removeIndex(i); usedMeshes.add(mesh); return mesh; } } vertexCount = 1 + (int)Short.MAX_VALUE; indexCount = Math.max(1 + (int)Short.MAX_VALUE, 1 << (32 - Integer.numberOfLeadingZeros(indexCount - 1))); Mesh result = new Mesh(false, vertexCount, indexCount, vertexAttributes); usedMeshes.add(result); return result; } @Override public void dispose () { for (Mesh m : usedMeshes) m.dispose(); usedMeshes.clear(); for (Mesh m : freeMeshes) m.dispose(); freeMeshes.clear(); } } /** A tight {@link MeshPool} implementation, which is typically used for static meshes (create once, use many). * @author Xoppa */ public static class TightMeshPool implements MeshPool { private Array<Mesh> freeMeshes = new Array<Mesh>(); private Array<Mesh> usedMeshes = new Array<Mesh>(); @Override public void flush () { freeMeshes.addAll(usedMeshes); usedMeshes.clear(); } @Override public Mesh obtain (VertexAttributes vertexAttributes, int vertexCount, int indexCount) { for (int i = 0, n = freeMeshes.size; i < n; ++i) { final Mesh mesh = freeMeshes.get(i); if (mesh.getVertexAttributes().equals(vertexAttributes) && mesh.getMaxVertices() == vertexCount && mesh.getMaxIndices() == indexCount) { freeMeshes.removeIndex(i); usedMeshes.add(mesh); return mesh; } } Mesh result = new Mesh(true, vertexCount, indexCount, vertexAttributes); usedMeshes.add(result); return result; } @Override public void dispose () { for (Mesh m : usedMeshes) m.dispose(); usedMeshes.clear(); for (Mesh m : freeMeshes) m.dispose(); freeMeshes.clear(); } } /** A {@link RenderableSorter} that sorts by vertex attributes, material attributes and primitive types (in that order), so that * meshes can be easily merged. * @author Xoppa */ public static class Sorter implements RenderableSorter, Comparator<Renderable> { @Override public void sort (Camera camera, Array<Renderable> renderables) { renderables.sort(this); } @Override public int compare (Renderable arg0, Renderable arg1) { final VertexAttributes va0 = arg0.meshPart.mesh.getVertexAttributes(); final VertexAttributes va1 = arg1.meshPart.mesh.getVertexAttributes(); final int vc = va0.compareTo(va1); if (vc == 0) { final int mc = arg0.material.compareTo(arg1.material); if (mc == 0) { return arg0.meshPart.primitiveType - arg1.meshPart.primitiveType; } return mc; } return vc; } } private Array<Renderable> renderables = new Array<Renderable>(); private FlushablePool<Renderable> renderablesPool = new FlushablePool<Renderable>() { @Override protected Renderable newObject () { return new Renderable(); } }; private FlushablePool<MeshPart> meshPartPool = new FlushablePool<MeshPart>() { @Override protected MeshPart newObject () { return new MeshPart(); } }; private Array<Renderable> items = new Array<Renderable>(); private Array<Renderable> tmp = new Array<Renderable>(); private MeshBuilder meshBuilder; private boolean building; private RenderableSorter sorter; private MeshPool meshPool; private Camera camera; /** Create a ModelCache using the default {@link Sorter} and the {@link SimpleMeshPool} implementation. This might not be the * most optimal implementation for you use-case, but should be good to start with. */ public ModelCache () { this(new Sorter(), new SimpleMeshPool()); } /** Create a ModelCache using the specified {@link RenderableSorter} and {@link MeshPool} implementation. The * {@link RenderableSorter} implementation will be called with the camera specified in {@link #begin(Camera)}. By default this * will be null. The sorter is important for optimizing the cache. For the best result, make sure that renderables that can be * merged are next to each other. */ public ModelCache (RenderableSorter sorter, MeshPool meshPool) { this.sorter = sorter; this.meshPool = meshPool; meshBuilder = new MeshBuilder(); } /** Begin creating the cache, must be followed by a call to {@link #end()}, in between these calls one or more calls to one of * the add(...) methods can be made. Calling this method will clear the cache and prepare it for creating a new cache. The * cache is not valid until the call to {@link #end()} is made. Use one of the add methods (e.g. {@link #add(Renderable)} or * {@link #add(RenderableProvider)}) to add renderables to the cache. */ public void begin () { begin(null); } /** Begin creating the cache, must be followed by a call to {@link #end()}, in between these calls one or more calls to one of * the add(...) methods can be made. Calling this method will clear the cache and prepare it for creating a new cache. The * cache is not valid until the call to {@link #end()} is made. Use one of the add methods (e.g. {@link #add(Renderable)} or * {@link #add(RenderableProvider)}) to add renderables to the cache. * @param camera The {@link Camera} that will passed to the {@link RenderableSorter} */ public void begin (Camera camera) { if (building) throw new GdxRuntimeException("Call end() after calling begin()"); building = true; this.camera = camera; renderablesPool.flush(); renderables.clear(); items.clear(); meshPartPool.flush(); meshPool.flush(); } private Renderable obtainRenderable (Material material, int primitiveType) { Renderable result = renderablesPool.obtain(); result.bones = null; result.environment = null; result.material = material; result.meshPart.mesh = null; result.meshPart.offset = 0; result.meshPart.size = 0; result.meshPart.primitiveType = primitiveType; result.meshPart.center.set(0, 0, 0); result.meshPart.halfExtents.set(0, 0, 0); result.meshPart.radius = -1f; result.shader = null; result.userData = null; result.worldTransform.idt(); return result; } /** Finishes creating the cache, must be called after a call to {@link #begin()}, only after this call the cache will be valid * (until the next call to {@link #begin()}). Calling this method will process all renderables added using one of the add(...) * methods and will combine them if possible. */ public void end () { if (!building) throw new GdxRuntimeException("Call begin() prior to calling end()"); building = false; if (items.size == 0) return; sorter.sort(camera, items); int itemCount = items.size; int initCount = renderables.size; final Renderable first = items.get(0); VertexAttributes vertexAttributes = first.meshPart.mesh.getVertexAttributes(); Material material = first.material; int primitiveType = first.meshPart.primitiveType; int offset = renderables.size; meshBuilder.begin(vertexAttributes); MeshPart part = meshBuilder.part("", primitiveType, meshPartPool.obtain()); renderables.add(obtainRenderable(material, primitiveType)); for (int i = 0, n = items.size; i < n; ++i) { final Renderable renderable = items.get(i); final VertexAttributes va = renderable.meshPart.mesh.getVertexAttributes(); final Material mat = renderable.material; final int pt = renderable.meshPart.primitiveType; final boolean sameMesh = va.equals(vertexAttributes) && renderable.meshPart.size + meshBuilder.getNumVertices() < Short.MAX_VALUE; // comparing indices and vertices... final boolean samePart = sameMesh && pt == primitiveType && mat.same(material, true); if (!samePart) { if (!sameMesh) { final Mesh mesh = meshBuilder.end(meshPool.obtain(vertexAttributes, meshBuilder.getNumVertices(), meshBuilder.getNumIndices())); while (offset < renderables.size) renderables.get(offset++).meshPart.mesh = mesh; meshBuilder.begin(vertexAttributes = va); } final MeshPart newPart = meshBuilder.part("", pt, meshPartPool.obtain()); final Renderable previous = renderables.get(renderables.size - 1); previous.meshPart.offset = part.offset; previous.meshPart.size = part.size; part = newPart; renderables.add(obtainRenderable(material = mat, primitiveType = pt)); } meshBuilder.setVertexTransform(renderable.worldTransform); meshBuilder.addMesh(renderable.meshPart.mesh, renderable.meshPart.offset, renderable.meshPart.size); } final Mesh mesh = meshBuilder.end(meshPool.obtain(vertexAttributes, meshBuilder.getNumVertices(), meshBuilder.getNumIndices())); while (offset < renderables.size) renderables.get(offset++).meshPart.mesh = mesh; final Renderable previous = renderables.get(renderables.size - 1); previous.meshPart.offset = part.offset; previous.meshPart.size = part.size; } /** Adds the specified {@link Renderable} to the cache. Must be called in between a call to {@link #begin()} and {@link #end()}. * All member objects might (depending on possibilities) be used by reference and should not change while the cache is used. If * the {@link Renderable#bones} member is not null then skinning is assumed and the renderable will be added as-is, by * reference. Otherwise the renderable will be merged with other renderables as much as possible, depending on the * {@link Mesh#getVertexAttributes()}, {@link Renderable#material} and primitiveType (in that order). The * {@link Renderable#environment}, {@link Renderable#shader} and {@link Renderable#userData} values (if any) are removed. * @param renderable The {@link Renderable} to add, should not change while the cache is needed. */ public void add (Renderable renderable) { if (!building) throw new GdxRuntimeException("Can only add items to the ModelCache in between .begin() and .end()"); if (renderable.bones == null) items.add(renderable); else renderables.add(renderable); } /** Adds the specified {@link RenderableProvider} to the cache, see {@link #add(Renderable)}. */ public void add (final RenderableProvider renderableProvider) { renderableProvider.getRenderables(tmp, renderablesPool); for (int i = 0, n = tmp.size; i < n; ++i) add(tmp.get(i)); tmp.clear(); } /** Adds the specified {@link RenderableProvider}s to the cache, see {@link #add(Renderable)}. */ public <T extends RenderableProvider> void add (final Iterable<T> renderableProviders) { for (final RenderableProvider renderableProvider : renderableProviders) add(renderableProvider); } @Override public void getRenderables (Array<Renderable> renderables, Pool<Renderable> pool) { if (building) throw new GdxRuntimeException("Cannot render a ModelCache in between .begin() and .end()"); for (Renderable r : this.renderables) { r.shader = null; r.environment = null; } renderables.addAll(this.renderables); } @Override public void dispose () { if (building) throw new GdxRuntimeException("Cannot dispose a ModelCache in between .begin() and .end()"); meshPool.dispose(); } }