ModelBatch - s76/libgdx GitHub Wiki
ModelBatch (code) is a class used for managing render calls. It is typically used to render instances of models, although it is not restricted to models. The ModelBatch class abstracts away all rendering code, providing a layer on top of it and allowing you to focus on more game specific logic. Every part of the ModelBatch functionality is customizable by design.
Caution: because ModelBatch manages the render calls and therefore the rendering context, you should not try to manually modify the render context (e.g. bind shaders, texture or meshes, or call any function starting with gl
) in between the ModelBatch.begin()
and ModelBatch.end()
calls. This will not work and might cause unpredictable behavior. Instead use the customization options that ModelBatch offers.
- ModelBatch is often compared to SpriteBatch. While this might be understandable from an API view, there are some very big differences making them less comparable. The main difference is that SpriteBatch merges multiple sprites into a single draw call, while ModelBatch doesn't combine render calls. This does have performance implications, so be aware to merge any render calls before sending them to the ModelBatch.
- ModelBatch does not perform (frustum) culling. It simply hasn't enough information to do this using the best performing method. By default, every call to
ModelBatch.render()
will at least lead to one actual render call. ModelBatch does allow you to customize this though and perform frustum culling prior to actually rendering. However, typically, you should perform (frustum) culling prior to callingModelBatch.render()
.
So what does ModelBatch actually do?
- It gathers render calls
- It gathers a shader for each render call
- It sorts the render calls
- It manages the rendering context
- It executes the render calls
That's it. Nothing more, nothing less. And every part of this is customizable. Please be aware that because of this design, there might be multiple ways to accomplish the same basic task.
ModelBatch is a relatively heavy weight object, because of the shaders it might create. When possible you should try to reuse it. You'd typically create a ModelBatch in the create()
method. Because it contains native resources (like the shaders it uses), you'll need to call the dispose()
method when no longer needed.
Rendering should be done every frame, typically in your render()
method. To start rendering you should call modelBatch.begin(camera);
. Next use the modelBatch.render(...);
method to add one or more render calls. When done adding render calls, you must call modelBatch.end();
to actually render to specified calls.
ModelBatch modelBatch;
...
@Override
public void create () {
modelBatch = new ModelBatch();
...
}
@Override
public void render () {
... // call glClear etc.
modelBatch.begin(camera);
modelBatch.render(...);
... // add other render calls
modelBatch.end();
...
}
@Override
public void dispose () {
modelBatch.dispose();
...
}
The call to modelBatch.render(...)
is only valid in between the call to modelBatch.begin(camera)
and modelBatch.end()
. The actual rendering is performed at the call to end();
. If you want to force rendering in between, then you can use the modelBatch.flush();
method.
The Camera
you supply is hold by reference, meaning that it must not be changed in between the begin and end calls. If you need to switch camera in between the begin and end calls, then you can call the modelBatch.setCamera(camera);
, which will flush()
the batch if needed.
The purpose of ModelBatch is to manage render calls. So what exactly is a render call and how do you specify them?
A "render call" (often also referred to as "draw call") is basically the instruction to the GPU to render (display) something. Simply said, each "render call" displays a shape with some properties (e.g. a location, image, color, etc). Or to be more precise, it instructs the GPU to render a given part of a mesh using a given shader in a given context. We will look more in depth on this later.
For basic usage, you don't have to know the exact details about a render call, because ModelBatch abstracts them away. However, there's a performance impact of render calls. Typically each render call is executed on the GPU, meaning that it is executed in parallel to CPU code. Whenever a new render call is executed, it might imply that GPU and CPU need to be synchronized. Or in other words, having are few large render calls is typically better performing than having many smaller render calls.
To specify a render call, libGDX contains the Renderable (code) class, which contains almost everything (except for the camera) required to perform a single render call. Basically it contains how (the shader) and where (the transformation) to render the shape (the mesh part) in which context (the environment and material).
The render(...)
method of ModelBatch
has many signatures (variations with different method arguments), one of which is ModelBatch.render(Renderable);
. Using this method you can directly specify the render call you want to add to the ModelBatch. The other render(...)
methods allow you to specify one or more render calls using a RenderableProvider
. In those methods, the other arguments let you provide default values for those render calls. For example, when using the ModelBatch#render(RenderableProvider, Environment, Shader)
method it will set (override) the environment
and shader
members of every Renderable
the RenderableProvider
provides.
RenderableProvider
(code) is an interface which you can implement to supply one or more Renderable
instances. Probably the most common implementation of this interface is the ModelInstance
class, which traverses all nodes (parts) and translates them into a Renderable
instance. However, you can use the RenderableProvider
anyway you need. For example, when creating a voxel engine, you could create a Renderable
for each chunk. Or when using an entity component system, you could use RenderableProvider
as component.
RenderableProvider
only has one method:
public void getRenderables (Array<Renderable> renderables, Pool<Renderable> pool);
This is a very open API (e.g. it gives you access to the Array
of all previously added Renderable
s), but you should restrict your usage to only adding elements to the array. The pool can optionally be used to avoid allocation. You're free to ignore it or to use it for any dynamic Renderable
needed. Any Renderable
you obtain()
from it, will be automatically be free'd by the ModelBatch
, you don't have to take care for that.
A Shader
(code) is an interface that abstracts the implementation of actually performing the render call. Typically its implementation uses a ShaderProgram
which is the GPU program (e.g. the vertex shader and fragment shader) needed to perform the render call. The Shader
implementation also contains everything needed to use this ShaderProgram
, like setting uniform values.
Independent of how the actual rendering is performed, the ModelBatch needs one Shader
per Renderable
. For this it uses the ShaderProvider
(code) interface. For every Renderable
added to the batch (even if it contains a shader), the getShader
of the ShaderProvider
will be called to fetch to shader to render it.
public Shader getShader (Renderable renderable);
By default the DefaultShaderProvider (code) is used, which will create a DefaultShader whenever a previous created shader can't be reused. You can, however, customize this by supplying your own ShaderProvider
or by extending the DefaultShaderProvider
.
ModelBatch
delegates managing Shader
's to the ShaderProviders
. Because a Shader
typically uses a ShaderProgram
, they need to be disposed. When modelBatch.dispose();
is called, ModelBatch
will call the dispose()
method the ShaderProvider
.
To help managing and reusing shaders, libGDX offers the abstract BaseShaderProvider
(code). This class keeps track of all shaders created, reuses them if possible and disposes them when no longer needed. If you extend this class, it will call the createShader(Renderable)
method when it hasn't got a shader it can reuse. Whether a Shader
can be reused, is determined by the call to shader.canRender(Renderable)
.
A typical use-case is to extend DefaultShaderProvider
(which extends BaseShaderProvider) and provide a custom shader when needed, while falling back to the DefaultShader when you can't use your custom shader.
public static class MyShaderProvider extends DefaultShaderProvider {
@Override
protected Shader createShader (Renderable renderable) {
if (renderable.material.has(CustomColorTypes.AlbedoColor))
return new MyShader(renderable);
else
return super.createShader(renderable);
}
}
Here the Material is used to decide whether the custom shader should be used. This is the preferred and easiest method. However, you can use any value, including the generic renderable.userData
to decide which shader to use, as long as its shader.canRender(renderable)
method returns true for the given renderable.
If render calls would be executed in a random order, then it would cause strange and less performing result. For example, if a transparent object would be rendered prior to an object that's behind it then you won't see the object behind it. This is because the depth buffer will prevent the object further away from being rendered. Sorting the render calls helps to solve this.
By default ModelBatch
will use the DefaultRenderableSorter (code) to sort the render calls. This implementation will cause that opaque objects are rendered first from front to back, after which transparent objects are rendered from back to front. To decide whether an object is transparent or not, the default implementation checks the BlendingAttribute#blended value.
Customizing sorting can help increase performance. For example, sorting based on shader, mesh or used textures might help decreasing shader, mesh or texture switches. These kind of optimizations are very application specific. You can customize sorting by specifying your own RenderableSorter
(code) implementation while constructing the ModelBatch
. This interface contains only one method:
public void sort (Camera camera, Array<Renderable> renderables);
This method provides all information the ModelBatch
has just before the actually rendering. It is also a very open API, you are allowed to modify the array as needed. This makes it possible to perform any last-minute actions (that might not be even related to sorting, like frustum culling) in this interface. The order of the renderables
after this method completes, will be the order in which the render calls will be actually executed.
ModelBatch
allows you to avoid redundant OpenGL calls, including texture binds, across multiple Shader
implementations. For example, when a Shader
requires backface culling and a previous shader enabled backface culling, then the redundant call to glEnable
and glCullFace
can be avoided.
The RenderContext
class tries to avoid these unnecessary calls. This class acts as a thin layer on top of OpenGL ES that keeps track of previous calls and therefore avoids making redundant calls. Only a small subset of the GL calls is implemented, but you can extend it to add additional calls. When not specified as argument in the constructor, ModelBatch
will create and manage a RenderContext
for you.
Caution: Obviously this will only work if all Shader
implementations use the RenderContext
instead of directly making GL calls. You should always use the RenderContext
if possible, instead of directly calling the corresponding GL call.
For example: When depth testing is enabled using the RenderContext, then it will enable depth testing for you. Now when you use e.g. SpriteBatch then that disables depth testing but doesn't update the RenderContext. This will lead to unexpected results. To avoid this, by default (when you don't specify a RenderContext yourself) ModelBatch will reset the RenderContext on both the begin()
and end()
methods, by calling the same named methods on the RenderContext. This is to make sure that context switches outside the ModelBatch don't interfere with the rendering.
However, when you specify your own RenderContext
(which doesn't have to be a custom implementation of it) then you're responsible for calling the context.begin()
and context.end()
methods. This allows you use the same context for multiple ModelBatch instances or even avoid having to reset the context all together.
If you specify a RenderContext on construction then you own that RenderContext and are expected to reset (call its begin() and end() methods) when needed. You can call the modelBatch.ownsRenderContext method to check whether the ModelBatch owns and manages the RenderContext.
To keep track of the textures currently being bound, RenderContext contains a textureBinder member. TextureBinder
(code) is an interface used to keep track of texture binds, as well as texture context (e.g. the minification/magnification filters). By default the DefaultTextureBinder
(code) is used. Although you can specify your own implementation, this is rarely required.
The DefaultTextureBinder uses every available texture unit (within the specified range) to avoid unneeded texture binds. A typical modern mobile GPU offers around 16 or 32 texture units to which textures can be bound. OpenGL ES limits the number of units to 32. You can specify the range to use when constructing the DefaultTextureBinder, using the offset
and count
arguments. If you don't specify an offset, then 0 is assumed. If you don't specify a count then all available remaining units will be used. ModelBatch will, by default, exclude texture unit 0 from the range, because this is often used for GUI. So by default, texture unit 1
to 31
will be used, unless the GPU supports less texture units.
DefaultTextureBinder
supports two methods:
- ROUNDROBIN: When a texture is already bound, then it is reused. Otherwise the first texture is bound to the first available unit, the next texture is bound to the next available unit, and so on. When all available units are used, then binding restarts at the first available unit, overwriting the previous bound texture.
- WEIGHTED: Weights the textures by counting the number of times a texture is used or not. Often reused textures are less likely to be overwritten, while less reused textures are more likely to be overwritten.
By default ModelBatch will use the WEIGHTED method.
You can bind a texture using TextureBinder
by calling the bind(...)
method. This method will return the unit the texture is bound to. So, practically, you can bind a texture in your Shader
to an uniform like this:
program.setUniformi(uniformLocation, context.textureBinder.bind(texture));
The api often uses a TextureDescriptor
when specifying a texture. This is because you might want to use a texture but do require specific context properties. These properties currently include the minification and magnification filters, as well as the horizontal wrapping and vertical wrapping. Therefor the TextureDescriptor is also used by e.g. the TextureAttribute and CubemapAttribute. For convenience, TextureBinder allows you to directly specify a TextureDescriptor:
program.setUniformi(uniformLocation, context.textureBinder.bind(
(TextureAttribute)(renderable.material.get(TextureAttribute.Diffuse)))
.textureDescription));