/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package org.illarion.engine.backend.gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack;
import com.badlogic.gdx.utils.Pools;
import illarion.common.types.Rectangle;
import org.illarion.engine.graphic.*;
import org.illarion.engine.graphic.effects.TextureEffect;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This is the graphics engine implementation that uses libGDX.
*
* @author Martin Karing <nitram@illarion.org>
*/
class GdxGraphics implements Graphics {
@Nonnull
private static final float[] FLT_BUFFER = new float[20];
/**
* The libGDX graphics instance that is used to display the graphics.
*/
@Nonnull
private final com.badlogic.gdx.Graphics gdxGraphics;
/**
* The sprite batch used to perform the batch rendering.
*/
@Nonnull
private final SpriteBatch spriteBatch;
/**
* This is a temporary color that is used only to transfer data to the libGDX functions.
*/
@Nonnull
private final com.badlogic.gdx.graphics.Color tempColor1;
/**
* This is a temporary color that is used only to transfer data to the libGDX functions.
*/
@Nonnull
private final com.badlogic.gdx.graphics.Color tempColor2;
/**
* This is a temporary color that is used only to transfer data to the libGDX functions.
*/
@Nonnull
private final com.badlogic.gdx.graphics.Color tempColor3;
/**
* This is a temporary color that is used only to transfer data to the libGDX functions.
*/
@Nonnull
private final com.badlogic.gdx.graphics.Color tempColor4;
/**
* The shape renderer used to draw primitive shapes.
*/
@Nonnull
private final ShapeRenderer shapeRenderer;
/**
* This is a temporary texture region instance that is used for some calculations.
*/
@Nonnull
private final TextureRegion tempRegion;
/**
* A temporary rectangle used for some calculations.
*/
@Nonnull
private final Rectangle tempEngineRectangle;
/**
* The camera that views the scene.
*/
@Nonnull
private final OrthographicCamera camera;
/**
* The engine implementation used for the rendering.
*/
@Nonnull
private final GdxEngine engine;
/**
* The blank background texture used to render rectangles.
*/
@Nullable
private Texture blankBackground;
/**
* This flag is set {@code true} in case the sprite batch rendering is currently activated.
*/
private boolean spriteBatchActive;
/**
* The blending mode that was applied last.
*/
@Nullable
private BlendingMode lastBlendingMode;
/**
* This is set {@code true} in case the clipping is activated.
*/
private boolean activeClipping;
/**
* Create a new instance of the graphics engine that is using libGDX to render.
*
* @param gdxGraphics the libGDX graphics instance that is used
*/
GdxGraphics(@Nonnull GdxEngine engine, @Nonnull com.badlogic.gdx.Graphics gdxGraphics) {
this.gdxGraphics = gdxGraphics;
this.engine = engine;
shapeRenderer = new ShapeRenderer();
spriteBatch = new SpriteBatch();
tempColor1 = new com.badlogic.gdx.graphics.Color();
tempColor2 = new com.badlogic.gdx.graphics.Color();
tempColor3 = new com.badlogic.gdx.graphics.Color();
tempColor4 = new com.badlogic.gdx.graphics.Color();
tempRegion = new TextureRegion();
tempEngineRectangle = new Rectangle();
camera = new OrthographicCamera();
camera.zoom = 1.f;
camera.setToOrtho(true);
}
@Nonnull
SpriteBatch getSpriteBatch() {
return spriteBatch;
}
void setCursor(@Nullable GdxCursor cursor) {
if (cursor == null) {
gdxGraphics.setCursor(null);
} else {
gdxGraphics.setCursor(cursor.getGdxCursor());
}
}
/**
* This function needs to be called before all rendering operations of a frame. It will setup the render system.
*/
void beginFrame() {
camera.setToOrtho(true, gdxGraphics.getWidth(), gdxGraphics.getHeight());
resetOffset();
clear();
lastBlendingMode = null;
setBlendingMode(BlendingMode.AlphaBlend);
if (blankBackground == null) {
blankBackground = engine.getAssets().getTextureManager().getTexture("gui/", "blank.png");
}
}
/**
* Reset any global offset applied.
*/
void resetOffset() {
applyOffset(0, 0);
}
@Override
public void clear() {
GL20 gl20 = gdxGraphics.getGL20();
gl20.glClearColor(0.f, 0.f, 0.f, 1.f);
gl20.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
}
@Override
public void drawSprite(
@Nonnull Sprite sprite,
int posX,
int posY,
@Nonnull Color color,
int frame,
double scale,
double rotation,
@Nonnull TextureEffect... effects) {
if (sprite instanceof GdxSprite) {
GdxSprite gdxSprite = (GdxSprite) sprite;
gdxSprite.getDisplayArea(posX, posY, scale, rotation, tempEngineRectangle);
double centerTransX = (gdxSprite.getWidth() * gdxSprite.getCenterX()) + (gdxSprite.getOffsetX() * scale);
double centerTransY = (gdxSprite.getHeight() * gdxSprite.getCenterY()) + (gdxSprite.getOffsetY() * scale);
activateSpriteBatch();
transferColor(color, tempColor1);
@Nullable GdxTextureEffect usedEffect;
if ((effects.length > 0) && (effects[0] instanceof GdxTextureEffect)) {
usedEffect = (GdxTextureEffect) effects[0];
} else {
usedEffect = null;
}
spriteBatch.setColor(tempColor1);
tempRegion.setRegion(gdxSprite.getFrame(frame).getTextureRegion());
tempRegion.flip(gdxSprite.isMirrored(), true);
if (usedEffect != null) {
float u, u2;
if (tempRegion.isFlipX()) {
u = tempRegion.getU();
u2 = tempRegion.getU2();
} else {
u2 = tempRegion.getU();
u = tempRegion.getU2();
}
float v, v2;
if (tempRegion.isFlipY()) {
v = tempRegion.getV();
v2 = tempRegion.getV2();
} else {
v2 = tempRegion.getV();
v = tempRegion.getV2();
}
usedEffect.setTopLeftCoordinate(u2, v2);
usedEffect.setBottomRightCoordinate(u, v);
usedEffect.activateEffect(spriteBatch);
}
spriteBatch.draw(tempRegion, tempEngineRectangle.getX(), tempEngineRectangle.getY(), (float) centerTransX,
(float) centerTransY, tempEngineRectangle.getWidth(), tempEngineRectangle.getHeight(), 1.f,
1.f, (float) rotation);
if (usedEffect != null) {
usedEffect.disableEffect(spriteBatch);
}
}
}
private void activateSpriteBatch() {
if (spriteBatchActive) {
return;
}
if (shapeRenderer.getCurrentType() != null) {
shapeRenderer.end();
}
spriteBatch.begin();
spriteBatchActive = true;
}
/**
* Transfer the color values from a game engine color instance to a libGDX color instance.
*
* @param source the engine color instance that is the source of the color data
* @param target the libGDX color instance that is the target of the color data
*/
static void transferColor(@Nonnull Color source, @Nonnull com.badlogic.gdx.graphics.Color target) {
target.set(source.getRedf(), source.getGreenf(), source.getBluef(), source.getAlphaf());
target.clamp();
}
@Override
public void drawTileSprite(@Nonnull Sprite sprite, int posX, int posY, @Nonnull Color topColor,
@Nonnull Color bottomColor, @Nonnull Color leftColor, @Nonnull Color rightColor,
@Nonnull Color centerColor, int frame, @Nonnull TextureEffect... effects) {
if (!(sprite instanceof GdxSprite)) {
throw new IllegalArgumentException("The sprite is expected to be a sprite provided by this engine.");
}
/*
This is something where libGDX provides no function for. So in this case we need to build the data array for
OpenGL by hand.
*/
GdxSprite gdxSprite = (GdxSprite) sprite;
activateSpriteBatch();
float bottomColorF = getFloatColor(bottomColor, tempColor1);
float topColorF = getFloatColor(topColor, tempColor1);
float leftColorF = getFloatColor(leftColor, tempColor1);
float rightColorF = getFloatColor(rightColor, tempColor1);
float centerColorF = getFloatColor(centerColor, tempColor1);
TextureRegion textureRegion = gdxSprite.getFrame(frame).getTextureRegion();
int width = sprite.getWidth() + 1;
int height = sprite.getHeight() + 1;
gdxSprite.getDisplayArea(posX, posY, 1.f, 0.f, tempEngineRectangle);
float centerTransX = (width * gdxSprite.getCenterX()) + gdxSprite.getOffsetX();
float centerTransY = (height * gdxSprite.getCenterY()) + gdxSprite.getOffsetY();
float originX = posX - centerTransX;
float originY = posY + centerTransY;
float topX = originX + (width / 2.f);
float topY = originY - height;
float topU = textureRegion.getU() + ((textureRegion.getU2() - textureRegion.getU()) / 2.f);
float topV = textureRegion.getV();
float leftX = originX;
float leftY = originY - (height / 2.f);
float leftU = textureRegion.getU();
float leftV = textureRegion.getV() + ((textureRegion.getV2() - textureRegion.getV()) / 2.f);
float rightX = originX + width;
float rightY = originY - (height / 2.f);
float rightU = textureRegion.getU2();
float rightV = textureRegion.getV() + ((textureRegion.getV2() - textureRegion.getV()) / 2.f);
float bottomX = originX + (width / 2.f);
float bottomY = originY;
float bottomU = textureRegion.getU() + ((textureRegion.getU2() - textureRegion.getU()) / 2.f);
float bottomV = textureRegion.getV2();
float[] vertices = FLT_BUFFER;
vertices[0] = topX;
vertices[1] = topY;
vertices[2] = topColorF;
vertices[3] = topU;
vertices[4] = topV;
vertices[5] = leftX;
vertices[6] = leftY;
vertices[7] = leftColorF;
vertices[8] = leftU;
vertices[9] = leftV;
vertices[10] = bottomX;
vertices[11] = bottomY;
vertices[12] = bottomColorF;
vertices[13] = bottomU;
vertices[14] = bottomV;
vertices[15] = rightX;
vertices[16] = rightY;
vertices[17] = rightColorF;
vertices[18] = rightU;
vertices[19] = rightV;
@Nullable GdxTextureEffect usedEffect;
if ((effects.length > 0) && (effects[0] instanceof GdxTextureEffect)) {
usedEffect = (GdxTextureEffect) effects[0];
} else {
usedEffect = null;
}
spriteBatch.setColor(tempColor1);
tempRegion.setRegion(gdxSprite.getFrame(frame).getTextureRegion());
tempRegion.flip(gdxSprite.isMirrored(), true);
if (usedEffect != null) {
float u, u2;
if (tempRegion.isFlipX()) {
u = tempRegion.getU();
u2 = tempRegion.getU2();
} else {
u2 = tempRegion.getU();
u = tempRegion.getU2();
}
float v, v2;
if (tempRegion.isFlipY()) {
v = tempRegion.getV();
v2 = tempRegion.getV2();
} else {
v2 = tempRegion.getV();
v = tempRegion.getV2();
}
usedEffect.setTopLeftCoordinate(u2, v2);
usedEffect.setBottomRightCoordinate(u, v);
usedEffect.activateEffect(spriteBatch);
}
spriteBatch.draw(textureRegion.getTexture(), FLT_BUFFER, 0, 20);
if (usedEffect != null) {
usedEffect.disableEffect(spriteBatch);
}
}
@Override
public void setBlendingMode(@Nonnull BlendingMode mode) {
if (lastBlendingMode == mode) {
return;
}
switch (mode) {
case AlphaBlend:
spriteBatch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
break;
case Multiply:
spriteBatch.setBlendFunction(GL20.GL_DST_COLOR, GL20.GL_ZERO);
break;
}
spriteBatch.enableBlending();
lastBlendingMode = mode;
}
@Override
public void drawText(
@Nonnull Font font, @Nonnull CharSequence text, @Nonnull Color color, int x, int y) {
drawText(font, text, color, x, y, 1.f, 1.f);
}
@Override
public void drawText(
@Nonnull Font font,
@Nonnull CharSequence text,
@Nonnull Color color,
int x,
int y,
double scaleX,
double scaleY) {
if (font instanceof GdxFont) {
activateSpriteBatch();
GdxFont gdxFont = (GdxFont) font;
BitmapFont bitmapFont = gdxFont.getBitmapFont();
bitmapFont.getData().setScale((float) scaleX, (float) scaleY);
transferColor(color, tempColor1);
bitmapFont.setColor(tempColor1);
GlyphLayout layout = Pools.obtain(GlyphLayout.class);
layout.setText(bitmapFont, text);
BitmapFont outlineFont = gdxFont.getOutlineBitmapFont();
if (outlineFont != null) {
transferColor(Color.BLACK, tempColor1);
tempColor1.a = color.getAlphaf();
outlineFont.getData().setScale((float) scaleX, (float) scaleY);
outlineFont.setColor(tempColor1);
GlyphLayout outlineLayout = Pools.obtain(GlyphLayout.class);
outlineLayout.setText(outlineFont, text);
float widthOffset = (layout.width - outlineLayout.width) / 2.f;
outlineFont.draw(spriteBatch, outlineLayout, x + widthOffset, y - outlineFont.getAscent());
Pools.free(outlineLayout);
}
bitmapFont.draw(spriteBatch, layout, x, y - bitmapFont.getAscent());
Pools.free(layout);
}
}
@Override
public void drawRectangle(int x, int y, int width, int height, @Nonnull Color color) {
activateSpriteBatch();
if (blankBackground == null) {
return;
}
drawTexture(blankBackground, x, y, width, height, color);
}
@Override
public void drawRectangle(@Nonnull Rectangle rectangle, @Nonnull Color color) {
drawRectangle(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight(), color);
}
@Override
public void drawRectangle(
int x,
int y,
int width,
int height,
@Nonnull Color topLeftColor,
@Nonnull Color topRightColor,
@Nonnull Color bottomLeftColor,
@Nonnull Color bottomRightColor) {
activateShapeRenderer();
transferColor(topLeftColor, tempColor1);
transferColor(topRightColor, tempColor2);
transferColor(bottomLeftColor, tempColor3);
transferColor(bottomRightColor, tempColor4);
shapeRenderer.rect(x, y, width, height, tempColor3, tempColor4, tempColor2, tempColor1);
shapeRenderer.end();
gdxGraphics.getGL20().glDisable(GL20.GL_BLEND);
}
private void activateShapeRenderer() {
if (shapeRenderer.getCurrentType() != null) {
return;
}
if (spriteBatchActive) {
spriteBatch.end();
spriteBatchActive = false;
}
GL20 gl20 = gdxGraphics.getGL20();
gl20.glEnable(GL20.GL_BLEND);
assert lastBlendingMode != null;
switch (lastBlendingMode) {
case AlphaBlend:
gl20.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
break;
case Multiply:
gl20.glBlendFunc(GL20.GL_DST_COLOR, GL20.GL_ZERO);
break;
}
shapeRenderer.begin(ShapeType.Filled);
}
@Override
public void drawTexture(
@Nonnull Texture texture,
int x,
int y,
int width,
int height,
@Nonnull Color color,
@Nonnull TextureEffect... effects) {
if ((width == 0) || (height == 0)) {
return;
}
if (texture instanceof GdxTexture) {
activateSpriteBatch();
transferColor(color, tempColor1);
@Nullable GdxTextureEffect usedEffect;
if ((effects.length > 0) && (effects[0] instanceof GdxTextureEffect)) {
usedEffect = (GdxTextureEffect) effects[0];
} else {
usedEffect = null;
}
if (usedEffect != null) {
usedEffect.activateEffect(spriteBatch);
}
spriteBatch.setColor(tempColor1);
tempRegion.setRegion(((GdxTexture) texture).getTextureRegion());
if (!tempRegion.isFlipY()) {
tempRegion.flip(false, true);
}
spriteBatch.draw(tempRegion, x, y, width, height);
if (usedEffect != null) {
usedEffect.disableEffect(spriteBatch);
}
}
}
@Override
public void drawTexture(
@Nonnull Texture texture,
int x,
int y,
int width,
int height,
int texX,
int texY,
int texWidth,
int texHeight,
@Nonnull Color color,
@Nonnull TextureEffect... effects) {
if ((width == 0) || (height == 0)) {
return;
}
if (color.getAlpha() == 0 && effects.length == 0) {
return;
}
if (texture instanceof GdxTexture) {
activateSpriteBatch();
transferColor(color, tempColor1);
@Nullable GdxTextureEffect usedEffect;
if ((effects.length > 0) && (effects[0] instanceof GdxTextureEffect)) {
usedEffect = (GdxTextureEffect) effects[0];
} else {
usedEffect = null;
}
if (usedEffect != null) {
usedEffect.activateEffect(spriteBatch);
}
spriteBatch.setColor(tempColor1);
tempRegion.setRegion(((GdxTexture) texture).getTextureRegion(), texX, texY, texWidth, texHeight);
if (!tempRegion.isFlipY()) {
tempRegion.flip(false, true);
}
spriteBatch.draw(tempRegion, x, y, width, height);
if (usedEffect != null) {
usedEffect.disableEffect(spriteBatch);
}
}
}
@Override
public void drawTexture(
@Nonnull Texture texture,
int x,
int y,
int width,
int height,
int texX,
int texY,
int texWidth,
int texHeight,
int centerX,
int centerY,
double rotate,
@Nonnull Color color,
@Nonnull TextureEffect... effects) {
if ((width == 0) || (height == 0)) {
return;
}
if (texture instanceof GdxTexture) {
activateSpriteBatch();
transferColor(color, tempColor1);
@Nullable GdxTextureEffect usedEffect;
if ((effects.length > 0) && (effects[0] instanceof GdxTextureEffect)) {
usedEffect = (GdxTextureEffect) effects[0];
} else {
usedEffect = null;
}
if (usedEffect != null) {
usedEffect.activateEffect(spriteBatch);
}
spriteBatch.setColor(tempColor1);
tempRegion.setRegion(((GdxTexture) texture).getTextureRegion(), texX, texY, texWidth, texHeight);
if (!tempRegion.isFlipY()) {
tempRegion.flip(false, true);
}
spriteBatch.draw(tempRegion, x, y, centerX, centerY, width, height, 1.f, 1.f, (float) rotate);
if (usedEffect != null) {
usedEffect.disableEffect(spriteBatch);
}
}
}
@Override
public void setClippingArea(int x, int y, int width, int height) {
if (activeClipping) {
unsetClippingArea();
}
if ((x == 0) && (y == 0) && (width == gdxGraphics.getWidth()) && (height == gdxGraphics.getHeight())) {
return;
}
flushAll();
com.badlogic.gdx.math.Rectangle clippingRect = Pools.obtain(com.badlogic.gdx.math.Rectangle.class);
clippingRect.set(x, y, width, height);
com.badlogic.gdx.math.Rectangle scissor = Pools.obtain(com.badlogic.gdx.math.Rectangle.class);
ScissorStack.calculateScissors(camera, 0, 0, gdxGraphics.getWidth(), gdxGraphics.getHeight(),
spriteBatch.getTransformMatrix(), clippingRect, scissor);
Pools.free(clippingRect);
if (ScissorStack.pushScissors(scissor)) {
activeClipping = true;
} else {
Pools.free(scissor);
}
}
@Override
public void unsetClippingArea() {
if (activeClipping) {
flushAll();
Pools.free(ScissorStack.popScissors());
activeClipping = false;
}
}
/**
* Stops the render operation of both the shape renderer and the sprite batch renderer to ensure that the
* buffered data is flushed to the screen.
*/
public void flushAll() {
if (shapeRenderer.getCurrentType() != null) {
shapeRenderer.end();
}
if (spriteBatchActive) {
spriteBatch.end();
spriteBatchActive = false;
}
}
private float getFloatColor(@Nonnull Color source, @Nonnull com.badlogic.gdx.graphics.Color workingInstance) {
transferColor(source, workingInstance);
return workingInstance.toFloatBits();
}
/**
* Apply a global offset to all following render operations.
*
* @param offsetX the x component of the offset
* @param offsetY the y component of the offset
*/
void applyOffset(int offsetX, int offsetY) {
camera.position.set((camera.viewportWidth / 2.f) + offsetX, (camera.viewportHeight / 2.f) + offsetY, 0.f);
camera.update();
spriteBatch.setProjectionMatrix(camera.combined);
shapeRenderer.setProjectionMatrix(camera.combined);
}
/**
* This function needs to be called after rendering a frame.
*/
void endFrame() {
flushAll();
unsetClippingArea();
}
}