-
Notifications
You must be signed in to change notification settings - Fork 30
Drawables and RenderStates
We now have seen some classes that implement the Drawable interface. Objects of those classes can be passed to a RenderTarget for drawing.
In this tutorial, we will implement a custom drawable class and learn more about vertices.
While the Drawable interface is very simple and only requests one method to be implemented, one must understand the synergy between Drawable and the RenderStates class. The render states contain information on how to draw objects.
This includes the blending mode, the transformation matrix, a texture (optional) and a shader (optional).
All this information is necessary, unlike in high-level objects like the Sprite class. It is because technically, only sets of vertices are actually drawn, and vertices (recall last chapter) are generally independent of the drawing (or rendering) state.
The transformation matrix is used to transform the vertices that are drawn. It is practically how Transformable objects are realized. Instead of maintaining the absolute vertex positions all the time, they regard their positions as relative to the origin. For drawing, they set the transformation matrix in the RenderStates accordingly, so all vertices are put in the place they belong. This also makes it possible to draw the same object (e.g. a sprite) at different positions.
The blending mode defines how the colors of the source object (either the vertex color or a pixel from a texture, see below) are applied to the destination (the render target). By default, we want to override the color on the target if no alpha value is set. If we do have an alpha value other than 255 (fully opaque), we want to blend the source and destination colors so it appears like the source color is drawn transparently. This behaviour is achieved using the ALPHA blending mode, which is the default. The BlendMode enumeration offers some more possible behaviours, which might be appropriate in different use cases.
Optionally, a texture can be applied to the render state. If this is the case, the vertex texture coordinates are used to determine the source color and the vertex color is used as a color mask on it (which allows sprite coloring, for instance).
Last, but not least, a shader can be used for rendering. However, shaders are the topic in the next tutorial chapter and do not concern us for now. Since the shader is optional, we can simply set it to null
.
The above is a lot of theory. As a practical example, we will create a simple re-implementation of the Sprite class, which draws a portion of a texture.
public class CustomSprite extends BasicTransformable implements Drawable
To make life easier, we take advantage of the BasicTransformable class by extending it. Doing so, our sprite will support all transformation methods that the original Sprite class does. Our job will only be to use the resulting transformation matrix for drawing.
Of course, we implement the Drawable interface.
private Texture texture = null;
private FloatRect textureRect = FloatRect.EMPTY;
private Color color = Color.WHITE;
private final VertexArray vertices = new VertexArray(PrimitiveType.QUADS);
These are all the member variables that we will need.
First off, since a sprite shows the portion of a texture, we do require the texture in question. We will store it in the texture
field. Furthermore, the textureRect
field will hold the portion of the texture to draw. We initialize it as EMPTY
, which will indicate that we will draw the whole texture. The color
field holds the sprite's color mask. Since we will always draw a rectangular portion of the texture, we need four vertices. For storing these, we use a VertexArray.
The transformation matrix is already inherited by extending the BasicTransformable class. We can get the current transformation matrix using the [getTransform](http://jsfml.org/javadoc/org/jsfml/graphics/BasicTransformable.html#getTransform(\)) method.
public void setTexture(Texture texture, FloatRect textureRect) {
this.texture = texture;
if(textureRect.equals(FloatRect.EMPTY) {
this.textureRect = new FloatRect(0, 0, texture.getSize().x, texture.getSize().y);
} else {
this.textureRect = textureRect;
}
recalculateVertices();
}
private void recalculateVertices() {
if(texture == null)
return;
//Get the texture's size as a floating point vector
Vector2f texSize = new Vector2f(texture.getSize());
//Calculate the normalized texture coordinates
Vector2f texTopLeft = Vector2f.componentwiseDiv(new Vector2f(textureRect.x, textureRect.y), texSize);
Vector2f texBottomLeft = Vector2f.componentwiseDiv(new Vector2f(textureRect.x, textureRect.y + textureRect.height), texSize);
Vector2f texBottomRight = Vector2f.componentwiseDiv(new Vector2f(textureRect.x + textureRect.width, textureRect.y + textureRect.height), texSize);
Vector2f texTopRight = Vector2f.componentwiseDiv(new Vector2f(textureRect.x + textureRect.width, textureRect.y), texSize);
//Initialize the vertices in counter-clockwise order
vertices.clear();
vertices.addVertex(new Vertex(new Vector2f(0, 0), color, texTopLeft);
vertices.addVertex(new Vertex(new Vector2f(0, textureRect.height), color, texBottomLeft);
vertices.addVertex(new Vertex(new Vector2f(textureRect.width, textureRect.height), color, texBottomRight);
vertices.addVertex(new Vertex(new Vector2f(textureRect.width, 0), color, texTopRight);
}
We will offer a method to set the sprite's texture and the texture rectangle. We do this in the same method here for simplicty's sake. When an empty texture rectangle is passed, we use the entire texture size instead.
After the texture has been changed, we need to re-calculate the sprite's vertices. This is because the texture coordinates of the vertices are normalized, that means they represent percentages of the texture's size. A texture coordinate of (0, 0)
represents the top left corner of the texture, (1, 1)
represents the bottom right corner.
The calculation looks more complicated than it really is. In a first step, we calculate the normalized texture coordinates of the source rectangle. Since they are relative to the texture's size, we can simply divide the pixel coordinates of the corners by the texture's size. As you see, the componentwiseMul operator of the Vector2f class is of great help here.
Then, we (re-)create the actual vertices. We put the top left vertex at (0, 0)
, our origin (remember, the transformations set by setPosition, setRotation etc are applied when drawing, using render states) and the bottom right vertex at the bottom right corner of our texture rectangle. That way, the sprite will appear exactly the size of the texture rectangle by default. We use the texture coordinates that we calculated before and set the color of all vertices to the sprite's color.
public void setColor(Color color) {
this.color = color;
//Recolor all existing vertices
for(int i = 0; i < vertices.size(); i++) {
Vertex old = vertices.get(i);
vertices.set(i, new Vertex(old.position, color, old.texCoords));
}
}
We also offer this method to set the sprite's color mask. After the color has been set, we need to apply the color to all existing vertices. We do this by creating copies of the old vertices and changing the color only. Since a Vertex is considered a primitive element in JSFML, it is immutable and does not have a setColor method, even though it may seem a little inconvenient in a case like this.
@Override
public void draw(RenderTarget target, RenderStates states) {
RenderStates newStates = new RenderStates(
states.blendMode,
Transform.combine(states.transform, this.getTransform()),
this.texture,
states.shader);
vertices.draw(target, newStates);
}
This method finally does the drawing of our sprite.
As you should notice, the draw
method gets passed not only the render target, but also a set of render states. These may be the default render states with no special information set, but this also allows for nested drawing. For instance, one could create a custom drawable that consists of multiple sprites sorrounding a common center. In such a case, the transformation (ie the translation to the common center) has to be retained.
This can be done by multiplying (combining) the transformation matrices, which is exactly what is done in the example above. The Transform class offers the [combine](http://jsfml.org/javadoc/org/jsfml/graphics/Transform.html#combine(org.jsfml.graphics.Transform, org.jsfml.graphics.Transform)) operator for this.
We put the combined transformation inside a new RenderStates object, so we won't modify the original one. That would not be possible anyway, since render states are also immutable for this exact reason. We override the texture used for drawing using our sprite's texture. The blending mode, and possibly a shader, are not defined in our sprite, and therefore we simply pass on what's given.
The draw
method then delegates to that of our vertex array, using the new render states. It will proceed to draw the vertices using that information.
The original Sprite also has methods to get the sprite's local and global axis-aligned boundaries. Using the vertex array's getBounds methods and combination of transformation matrices, this is a relatively easy task.
Other than that, we have implemented an almost exact copy of the original Sprite class.