/*******************************************************************************
* 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.g2d;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Affine2;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.utils.GdxRuntimeException;
/** CpuSpriteBatch behaves like SpriteBatch, except it doesn't flush automatically whenever the transformation matrix changes.
* Instead, the vertices get adjusted on subsequent draws to match the running batch. This can improve performance through longer
* batches, for example when drawing Groups with transform enabled.
*
* @see SpriteBatch#renderCalls
* @see com.badlogic.gdx.scenes.scene2d.Group#setTransform(boolean) Group.setTransform()
* @author Valentin Milea */
public class CpuSpriteBatch extends SpriteBatch {
private final Matrix4 virtualMatrix = new Matrix4();
private final Affine2 adjustAffine = new Affine2();
private boolean adjustNeeded;
private boolean haveIdentityRealMatrix = true;
private final Affine2 tmpAffine = new Affine2();
/** Constructs a CpuSpriteBatch with a size of 1000 and the default shader.
* @see SpriteBatch#SpriteBatch() */
public CpuSpriteBatch () {
this(1000);
}
/** Constructs a CpuSpriteBatch with the default shader.
* @see SpriteBatch#SpriteBatch(int) */
public CpuSpriteBatch (int size) {
this(size, null);
}
/** Constructs a CpuSpriteBatch with a custom shader.
* @see SpriteBatch#SpriteBatch(int, ShaderProgram) */
public CpuSpriteBatch (int size, ShaderProgram defaultShader) {
super(size, defaultShader);
}
/** <p>
* Flushes the batch and realigns the real matrix on the GPU. Subsequent draws won't need adjustment and will be slightly
* faster as long as the transform matrix is not {@link #setTransformMatrix(Matrix4) changed}.
* </p>
* <p>
* Note: The real transform matrix <em>must</em> be invertible. If a singular matrix is detected, GdxRuntimeException will be
* thrown.
* </p>
* @see SpriteBatch#flush() */
public void flushAndSyncTransformMatrix () {
flush();
if (adjustNeeded) {
// vertices flushed, safe now to replace matrix
haveIdentityRealMatrix = checkIdt(virtualMatrix);
if (!haveIdentityRealMatrix && virtualMatrix.det() == 0)
throw new GdxRuntimeException("Transform matrix is singular, can't sync");
adjustNeeded = false;
super.setTransformMatrix(virtualMatrix);
}
}
@Override
public Matrix4 getTransformMatrix () {
return (adjustNeeded ? virtualMatrix : super.getTransformMatrix());
}
/** Sets the transform matrix to be used by this Batch. Even if this is called inside a {@link #begin()}/{@link #end()} block,
* the current batch is <em>not</em> flushed to the GPU. Instead, for every subsequent draw() the vertices will be transformed
* on the CPU to match the original batch matrix. This adjustment must be performed until the matrices are realigned by
* restoring the original matrix, or by calling {@link #flushAndSyncTransformMatrix()}. */
@Override
public void setTransformMatrix (Matrix4 transform) {
Matrix4 realMatrix = super.getTransformMatrix();
if (checkEqual(realMatrix, transform)) {
adjustNeeded = false;
} else {
if (isDrawing()) {
virtualMatrix.setAsAffine(transform);
adjustNeeded = true;
// adjust = inverse(real) x virtual
// real x adjust x vertex = virtual x vertex
if (haveIdentityRealMatrix) {
adjustAffine.set(transform);
} else {
tmpAffine.set(transform);
adjustAffine.set(realMatrix).inv().mul(tmpAffine);
}
} else {
realMatrix.setAsAffine(transform);
haveIdentityRealMatrix = checkIdt(realMatrix);
}
}
}
/** Sets the transform matrix to be used by this Batch. Even if this is called inside a {@link #begin()}/{@link #end()} block,
* the current batch is <em>not</em> flushed to the GPU. Instead, for every subsequent draw() the vertices will be transformed
* on the CPU to match the original batch matrix. This adjustment must be performed until the matrices are realigned by
* restoring the original matrix, or by calling {@link #flushAndSyncTransformMatrix()} or {@link #end()}. */
public void setTransformMatrix (Affine2 transform) {
Matrix4 realMatrix = super.getTransformMatrix();
if (checkEqual(realMatrix, transform)) {
adjustNeeded = false;
} else {
virtualMatrix.setAsAffine(transform);
if (isDrawing()) {
adjustNeeded = true;
// adjust = inverse(real) x virtual
// real x adjust x vertex = virtual x vertex
if (haveIdentityRealMatrix) {
adjustAffine.set(transform);
} else {
adjustAffine.set(realMatrix).inv().mul(transform);
}
} else {
realMatrix.setAsAffine(transform);
haveIdentityRealMatrix = checkIdt(realMatrix);
}
}
}
@Override
public void draw (Texture texture, float x, float y, float originX, float originY, float width, float height, float scaleX,
float scaleY, float rotation, int srcX, int srcY, int srcWidth, int srcHeight, boolean flipX, boolean flipY) {
if (!adjustNeeded) {
super.draw(texture, x, y, originX, originY, width, height, scaleX, scaleY, rotation, srcX, srcY, srcWidth, srcHeight,
flipX, flipY);
} else {
drawAdjusted(texture, x, y, originX, originY, width, height, scaleX, scaleY, rotation, srcX, srcY, srcWidth, srcHeight,
flipX, flipY);
}
}
@Override
public void draw (Texture texture, float x, float y, float width, float height, int srcX, int srcY, int srcWidth,
int srcHeight, boolean flipX, boolean flipY) {
if (!adjustNeeded) {
super.draw(texture, x, y, width, height, srcX, srcY, srcWidth, srcHeight, flipX, flipY);
} else {
drawAdjusted(texture, x, y, 0, 0, width, height, 1, 1, 0, srcX, srcY, srcWidth, srcHeight, flipX, flipY);
}
}
@Override
public void draw (Texture texture, float x, float y, int srcX, int srcY, int srcWidth, int srcHeight) {
if (!adjustNeeded) {
super.draw(texture, x, y, srcX, srcY, srcWidth, srcHeight);
} else {
drawAdjusted(texture, x, y, 0, 0, srcWidth, srcHeight, 1, 1, 0, srcX, srcY, srcWidth, srcHeight,
false, false);
}
}
@Override
public void draw (Texture texture, float x, float y, float width, float height, float u, float v, float u2, float v2) {
if (!adjustNeeded) {
super.draw(texture, x, y, width, height, u, v, u2, v2);
} else {
drawAdjustedUV(texture, x, y, 0, 0, width, height, 1, 1, 0, u, v, u2, v2, false, false);
}
}
@Override
public void draw (Texture texture, float x, float y) {
if (!adjustNeeded) {
super.draw(texture, x, y);
} else {
drawAdjusted(texture, x, y, 0, 0, texture.getWidth(), texture.getHeight(), 1, 1, 0, 0, 1, 1, 0, false, false);
}
}
@Override
public void draw (Texture texture, float x, float y, float width, float height) {
if (!adjustNeeded) {
super.draw(texture, x, y, width, height);
} else {
drawAdjusted(texture, x, y, 0, 0, width, height, 1, 1, 0, 0, 1, 1, 0, false, false);
}
}
@Override
public void draw (TextureRegion region, float x, float y) {
if (!adjustNeeded) {
super.draw(region, x, y);
} else {
drawAdjusted(region, x, y, 0, 0, region.getRegionWidth(), region.getRegionHeight(), 1, 1, 0);
}
}
@Override
public void draw (TextureRegion region, float x, float y, float width, float height) {
if (!adjustNeeded) {
super.draw(region, x, y, width, height);
} else {
drawAdjusted(region, x, y, 0, 0, width, height, 1, 1, 0);
}
}
@Override
public void draw (TextureRegion region, float x, float y, float originX, float originY, float width, float height,
float scaleX, float scaleY, float rotation) {
if (!adjustNeeded) {
super.draw(region, x, y, originX, originY, width, height, scaleX, scaleY, rotation);
} else {
drawAdjusted(region, x, y, originX, originY, width, height, scaleX, scaleY, rotation);
}
}
@Override
public void draw (TextureRegion region, float x, float y, float originX, float originY, float width, float height,
float scaleX, float scaleY, float rotation, boolean clockwise) {
if (!adjustNeeded) {
super.draw(region, x, y, originX, originY, width, height, scaleX, scaleY, rotation, clockwise);
} else {
drawAdjusted(region, x, y, originX, originY, width, height, scaleX, scaleY, rotation, clockwise);
}
}
@Override
public void draw (Texture texture, float[] spriteVertices, int offset, int count) {
if (count % Sprite.SPRITE_SIZE != 0) throw new GdxRuntimeException("invalid vertex count");
if (!adjustNeeded) {
super.draw(texture, spriteVertices, offset, count);
} else {
drawAdjusted(texture, spriteVertices, offset, count);
}
}
@Override
public void draw (TextureRegion region, float width, float height, Affine2 transform) {
if (!adjustNeeded) {
super.draw(region, width, height, transform);
} else {
drawAdjusted(region, width, height, transform);
}
}
private void drawAdjusted (TextureRegion region, float x, float y, float originX, float originY, float width, float height,
float scaleX, float scaleY, float rotation) {
// v must be flipped
drawAdjustedUV(region.texture, x, y, originX, originY, width, height, scaleX, scaleY, rotation, region.u, region.v2,
region.u2, region.v, false, false);
}
private void drawAdjusted (Texture texture, float x, float y, float originX, float originY, float width, float height,
float scaleX, float scaleY, float rotation, int srcX, int srcY, int srcWidth, int srcHeight, boolean flipX, boolean flipY) {
float invTexWidth = 1.0f / texture.getWidth();
float invTexHeight = 1.0f / texture.getHeight();
float u = srcX * invTexWidth;
float v = (srcY + srcHeight) * invTexHeight;
float u2 = (srcX + srcWidth) * invTexWidth;
float v2 = srcY * invTexHeight;
drawAdjustedUV(texture, x, y, originX, originY, width, height, scaleX, scaleY, rotation, u, v, u2, v2, flipX, flipY);
}
private void drawAdjustedUV (Texture texture, float x, float y, float originX, float originY, float width, float height,
float scaleX, float scaleY, float rotation, float u, float v, float u2, float v2, boolean flipX, boolean flipY) {
if (!drawing) throw new IllegalStateException("CpuSpriteBatch.begin must be called before draw.");
if (texture != lastTexture)
switchTexture(texture);
else if (idx == vertices.length) super.flush();
// bottom left and top right corner points relative to origin
final float worldOriginX = x + originX;
final float worldOriginY = y + originY;
float fx = -originX;
float fy = -originY;
float fx2 = width - originX;
float fy2 = height - originY;
// scale
if (scaleX != 1 || scaleY != 1) {
fx *= scaleX;
fy *= scaleY;
fx2 *= scaleX;
fy2 *= scaleY;
}
// construct corner points, start from top left and go counter clockwise
final float p1x = fx;
final float p1y = fy;
final float p2x = fx;
final float p2y = fy2;
final float p3x = fx2;
final float p3y = fy2;
final float p4x = fx2;
final float p4y = fy;
float x1;
float y1;
float x2;
float y2;
float x3;
float y3;
float x4;
float y4;
// rotate
if (rotation != 0) {
final float cos = MathUtils.cosDeg(rotation);
final float sin = MathUtils.sinDeg(rotation);
x1 = cos * p1x - sin * p1y;
y1 = sin * p1x + cos * p1y;
x2 = cos * p2x - sin * p2y;
y2 = sin * p2x + cos * p2y;
x3 = cos * p3x - sin * p3y;
y3 = sin * p3x + cos * p3y;
x4 = x1 + (x3 - x2);
y4 = y3 - (y2 - y1);
} else {
x1 = p1x;
y1 = p1y;
x2 = p2x;
y2 = p2y;
x3 = p3x;
y3 = p3y;
x4 = p4x;
y4 = p4y;
}
x1 += worldOriginX;
y1 += worldOriginY;
x2 += worldOriginX;
y2 += worldOriginY;
x3 += worldOriginX;
y3 += worldOriginY;
x4 += worldOriginX;
y4 += worldOriginY;
if (flipX) {
float tmp = u;
u = u2;
u2 = tmp;
}
if (flipY) {
float tmp = v;
v = v2;
v2 = tmp;
}
Affine2 t = adjustAffine;
vertices[idx + 0] = t.m00 * x1 + t.m01 * y1 + t.m02;
vertices[idx + 1] = t.m10 * x1 + t.m11 * y1 + t.m12;
vertices[idx + 2] = color;
vertices[idx + 3] = u;
vertices[idx + 4] = v;
vertices[idx + 5] = t.m00 * x2 + t.m01 * y2 + t.m02;
vertices[idx + 6] = t.m10 * x2 + t.m11 * y2 + t.m12;
vertices[idx + 7] = color;
vertices[idx + 8] = u;
vertices[idx + 9] = v2;
vertices[idx + 10] = t.m00 * x3 + t.m01 * y3 + t.m02;
vertices[idx + 11] = t.m10 * x3 + t.m11 * y3 + t.m12;
vertices[idx + 12] = color;
vertices[idx + 13] = u2;
vertices[idx + 14] = v2;
vertices[idx + 15] = t.m00 * x4 + t.m01 * y4 + t.m02;
vertices[idx + 16] = t.m10 * x4 + t.m11 * y4 + t.m12;
vertices[idx + 17] = color;
vertices[idx + 18] = u2;
vertices[idx + 19] = v;
idx += Sprite.SPRITE_SIZE;
}
private void drawAdjusted (TextureRegion region, float x, float y, float originX, float originY, float width, float height,
float scaleX, float scaleY, float rotation, boolean clockwise) {
if (!drawing) throw new IllegalStateException("CpuSpriteBatch.begin must be called before draw.");
if (region.texture != lastTexture)
switchTexture(region.texture);
else if (idx == vertices.length) super.flush();
// bottom left and top right corner points relative to origin
final float worldOriginX = x + originX;
final float worldOriginY = y + originY;
float fx = -originX;
float fy = -originY;
float fx2 = width - originX;
float fy2 = height - originY;
// scale
if (scaleX != 1 || scaleY != 1) {
fx *= scaleX;
fy *= scaleY;
fx2 *= scaleX;
fy2 *= scaleY;
}
// construct corner points, start from top left and go counter clockwise
final float p1x = fx;
final float p1y = fy;
final float p2x = fx;
final float p2y = fy2;
final float p3x = fx2;
final float p3y = fy2;
final float p4x = fx2;
final float p4y = fy;
float x1;
float y1;
float x2;
float y2;
float x3;
float y3;
float x4;
float y4;
// rotate
if (rotation != 0) {
final float cos = MathUtils.cosDeg(rotation);
final float sin = MathUtils.sinDeg(rotation);
x1 = cos * p1x - sin * p1y;
y1 = sin * p1x + cos * p1y;
x2 = cos * p2x - sin * p2y;
y2 = sin * p2x + cos * p2y;
x3 = cos * p3x - sin * p3y;
y3 = sin * p3x + cos * p3y;
x4 = x1 + (x3 - x2);
y4 = y3 - (y2 - y1);
} else {
x1 = p1x;
y1 = p1y;
x2 = p2x;
y2 = p2y;
x3 = p3x;
y3 = p3y;
x4 = p4x;
y4 = p4y;
}
x1 += worldOriginX;
y1 += worldOriginY;
x2 += worldOriginX;
y2 += worldOriginY;
x3 += worldOriginX;
y3 += worldOriginY;
x4 += worldOriginX;
y4 += worldOriginY;
float u1, v1, u2, v2, u3, v3, u4, v4;
if (clockwise) {
u1 = region.u2;
v1 = region.v2;
u2 = region.u;
v2 = region.v2;
u3 = region.u;
v3 = region.v;
u4 = region.u2;
v4 = region.v;
} else {
u1 = region.u;
v1 = region.v;
u2 = region.u2;
v2 = region.v;
u3 = region.u2;
v3 = region.v2;
u4 = region.u;
v4 = region.v2;
}
Affine2 t = adjustAffine;
vertices[idx + 0] = t.m00 * x1 + t.m01 * y1 + t.m02;
vertices[idx + 1] = t.m10 * x1 + t.m11 * y1 + t.m12;
vertices[idx + 2] = color;
vertices[idx + 3] = u1;
vertices[idx + 4] = v1;
vertices[idx + 5] = t.m00 * x2 + t.m01 * y2 + t.m02;
vertices[idx + 6] = t.m10 * x2 + t.m11 * y2 + t.m12;
vertices[idx + 7] = color;
vertices[idx + 8] = u2;
vertices[idx + 9] = v2;
vertices[idx + 10] = t.m00 * x3 + t.m01 * y3 + t.m02;
vertices[idx + 11] = t.m10 * x3 + t.m11 * y3 + t.m12;
vertices[idx + 12] = color;
vertices[idx + 13] = u3;
vertices[idx + 14] = v3;
vertices[idx + 15] = t.m00 * x4 + t.m01 * y4 + t.m02;
vertices[idx + 16] = t.m10 * x4 + t.m11 * y4 + t.m12;
vertices[idx + 17] = color;
vertices[idx + 18] = u4;
vertices[idx + 19] = v4;
idx += Sprite.SPRITE_SIZE;
}
private void drawAdjusted (TextureRegion region, float width, float height, Affine2 transform) {
if (!drawing) throw new IllegalStateException("CpuSpriteBatch.begin must be called before draw.");
if (region.texture != lastTexture)
switchTexture(region.texture);
else if (idx == vertices.length) super.flush();
Affine2 t = transform;
// construct corner points
float x1 = t.m02;
float y1 = t.m12;
float x2 = t.m01 * height + t.m02;
float y2 = t.m11 * height + t.m12;
float x3 = t.m00 * width + t.m01 * height + t.m02;
float y3 = t.m10 * width + t.m11 * height + t.m12;
float x4 = t.m00 * width + t.m02;
float y4 = t.m10 * width + t.m12;
// v must be flipped
float u = region.u;
float v = region.v2;
float u2 = region.u2;
float v2 = region.v;
t = adjustAffine;
vertices[idx + 0] = t.m00 * x1 + t.m01 * y1 + t.m02;
vertices[idx + 1] = t.m10 * x1 + t.m11 * y1 + t.m12;
vertices[idx + 2] = color;
vertices[idx + 3] = u;
vertices[idx + 4] = v;
vertices[idx + 5] = t.m00 * x2 + t.m01 * y2 + t.m02;
vertices[idx + 6] = t.m10 * x2 + t.m11 * y2 + t.m12;
vertices[idx + 7] = color;
vertices[idx + 8] = u;
vertices[idx + 9] = v2;
vertices[idx + 10] = t.m00 * x3 + t.m01 * y3 + t.m02;
vertices[idx + 11] = t.m10 * x3 + t.m11 * y3 + t.m12;
vertices[idx + 12] = color;
vertices[idx + 13] = u2;
vertices[idx + 14] = v2;
vertices[idx + 15] = t.m00 * x4 + t.m01 * y4 + t.m02;
vertices[idx + 16] = t.m10 * x4 + t.m11 * y4 + t.m12;
vertices[idx + 17] = color;
vertices[idx + 18] = u2;
vertices[idx + 19] = v;
idx += Sprite.SPRITE_SIZE;
}
private void drawAdjusted (Texture texture, float[] spriteVertices, int offset, int count) {
if (!drawing) throw new IllegalStateException("CpuSpriteBatch.begin must be called before draw.");
if (texture != lastTexture) switchTexture(texture);
Affine2 t = adjustAffine;
int copyCount = Math.min(vertices.length - idx, count);
do {
count -= copyCount;
while (copyCount > 0) {
float x = spriteVertices[offset];
float y = spriteVertices[offset + 1];
vertices[idx] = t.m00 * x + t.m01 * y + t.m02; // x
vertices[idx + 1] = t.m10 * x + t.m11 * y + t.m12; // y
vertices[idx + 2] = spriteVertices[offset + 2]; // color
vertices[idx + 3] = spriteVertices[offset + 3]; // u
vertices[idx + 4] = spriteVertices[offset + 4]; // v
idx += Sprite.VERTEX_SIZE;
offset += Sprite.VERTEX_SIZE;
copyCount -= Sprite.VERTEX_SIZE;
}
if (count > 0) {
super.flush();
copyCount = Math.min(vertices.length, count);
}
} while (count > 0);
}
private static boolean checkEqual (Matrix4 a, Matrix4 b) {
if (a == b) return true;
// matrices are assumed to be 2D transformations
return (a.val[Matrix4.M00] == b.val[Matrix4.M00] && a.val[Matrix4.M10] == b.val[Matrix4.M10]
&& a.val[Matrix4.M01] == b.val[Matrix4.M01] && a.val[Matrix4.M11] == b.val[Matrix4.M11]
&& a.val[Matrix4.M03] == b.val[Matrix4.M03] && a.val[Matrix4.M13] == b.val[Matrix4.M13]);
}
private static boolean checkEqual (Matrix4 matrix, Affine2 affine) {
final float[] val = matrix.getValues();
// matrix is assumed to be 2D transformation
return (val[Matrix4.M00] == affine.m00 && val[Matrix4.M10] == affine.m10 && val[Matrix4.M01] == affine.m01
&& val[Matrix4.M11] == affine.m11 && val[Matrix4.M03] == affine.m02 && val[Matrix4.M13] == affine.m12);
}
private static boolean checkIdt (Matrix4 matrix) {
final float[] val = matrix.getValues();
// matrix is assumed to be 2D transformation
return (val[Matrix4.M00] == 1 && val[Matrix4.M10] == 0 && val[Matrix4.M01] == 0 && val[Matrix4.M11] == 1
&& val[Matrix4.M03] == 0 && val[Matrix4.M13] == 0);
}
}