/*******************************************************************************
* Copyright 2012 bmanuel
*
* 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.bitfire.postprocessing;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.bitfire.postprocessing.utils.PingPongBuffer;
import com.bitfire.utils.ItemsManager;
/** Provides a way to capture the rendered scene to an off-screen buffer and to apply a chain of effects on it before rendering to
* screen.
*
* Effects can be added or removed via {@link #addEffect(PostProcessorEffect)} and {@link #removeEffect(PostProcessorEffect)}.
*
* @author bmanuel */
public final class PostProcessor implements Disposable {
/** Enable pipeline state queries: beware the pipeline can stall! */
public static boolean EnableQueryStates = false;
private static PipelineState pipelineState = null;
private static Format fbFormat;
public final PingPongBuffer composite;
private TextureWrap compositeWrapU;
private TextureWrap compositeWrapV;
private final ItemsManager<PostProcessorEffect> effectsManager = new ItemsManager<PostProcessorEffect>();
private static final Array<PingPongBuffer> buffers = new Array<PingPongBuffer>(5);
private final Color clearColor = Color.CLEAR;
private int clearBits = GL20.GL_COLOR_BUFFER_BIT;
private float clearDepth = 1f;
private static Rectangle viewport = new Rectangle();
private static boolean hasViewport = false;
private boolean enabled = true;
private boolean capturing = false;
private boolean hasCaptured = false;
private boolean useDepth = false;
private PostProcessorListener listener = null;
// maintains a per-frame updated list of enabled effects
public Array<PostProcessorEffect> enabledEffects = new Array<PostProcessorEffect>(5);
/** Construct a new PostProcessor with FBO dimensions set to the size of the screen */
public PostProcessor (boolean useDepth, boolean useAlphaChannel, boolean use32Bits) {
this(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), useDepth, useAlphaChannel, use32Bits);
}
/** Construct a new PostProcessor with the given parameters, defaulting to <em>TextureWrap.ClampToEdge</em> as texture wrap mode */
public PostProcessor (int fboWidth, int fboHeight, boolean useDepth, boolean useAlphaChannel, boolean use32Bits) {
this(fboWidth, fboHeight, useDepth, useAlphaChannel, use32Bits, TextureWrap.ClampToEdge, TextureWrap.ClampToEdge);
}
/** Construct a new PostProcessor with the given parameters and viewport, defaulting to <em>TextureWrap.ClampToEdge</em> as
* texture wrap mode */
public PostProcessor (Rectangle viewport, boolean useDepth, boolean useAlphaChannel, boolean use32Bits) {
this((int)viewport.width, (int)viewport.height, useDepth, useAlphaChannel, use32Bits, TextureWrap.ClampToEdge,
TextureWrap.ClampToEdge);
setViewport(viewport);
}
/** Construct a new PostProcessor with the given parameters, viewport and the specified texture wrap mode */
public PostProcessor (Rectangle viewport, boolean useDepth, boolean useAlphaChannel, boolean use32Bits, TextureWrap u,
TextureWrap v) {
this((int)viewport.width, (int)viewport.height, useDepth, useAlphaChannel, use32Bits, u, v);
setViewport(viewport);
}
/** Construct a new PostProcessor with the given parameters and the specified texture wrap mode */
public PostProcessor (int fboWidth, int fboHeight, boolean useDepth, boolean useAlphaChannel, boolean use32Bits,
TextureWrap u, TextureWrap v) {
if (use32Bits) {
if (useAlphaChannel) {
fbFormat = Format.RGBA8888;
} else {
fbFormat = Format.RGB888;
}
} else {
if (useAlphaChannel) {
fbFormat = Format.RGBA4444;
} else {
fbFormat = Format.RGB565;
}
}
composite = newPingPongBuffer(fboWidth, fboHeight, fbFormat, useDepth);
setBufferTextureWrap(u, v);
pipelineState = new PipelineState();
capturing = false;
hasCaptured = false;
enabled = true;
this.useDepth = useDepth;
if (useDepth) {
clearBits |= GL20.GL_DEPTH_BUFFER_BIT;
}
setViewport(null);
}
/** Creates and returns a managed PingPongBuffer buffer, just create and forget. If rebind() is called on context loss, managed
* PingPongBuffers will be rebound for you.
*
* This is a drop-in replacement for the same-signature PingPongBuffer's constructor. */
public static PingPongBuffer newPingPongBuffer (int width, int height, Format frameBufferFormat, boolean hasDepth) {
PingPongBuffer buffer = new PingPongBuffer(width, height, frameBufferFormat, hasDepth);
buffers.add(buffer);
return buffer;
}
/** Provides a way to query the pipeline for the most used states */
public static boolean isStateEnabled (int pname) {
if (EnableQueryStates) {
// Gdx.app.log( "PipelineState", "Querying blending" );
return pipelineState.isEnabled(pname);
}
// Gdx.app.log( "PipelineState", "(not querying)" );
return false;
}
/** Sets the viewport to be restored, if null is specified then the viewport will NOT be restored at all.
*
* The predefined effects will restore the viewport settings at the final blitting stage (render to screen) by invoking the
* restoreViewport static method. */
public void setViewport (Rectangle viewport) {
PostProcessor.hasViewport = (viewport != null);
if (hasViewport) {
PostProcessor.viewport.set(viewport);
}
}
/** Frees owned resources. */
@Override
public void dispose () {
effectsManager.dispose();
// cleanup managed buffers, if any
for (int i = 0; i < buffers.size; i++) {
buffers.get(i).dispose();
}
buffers.clear();
if (enabledEffects != null) {
enabledEffects.clear();
}
pipelineState.dispose();
}
/** Whether or not the post-processor is enabled */
public boolean isEnabled () {
return enabled;
}
/** If called before capturing it will indicate if the next capture call will succeeds or not. */
public boolean isReady () {
boolean hasEffects = false;
for (PostProcessorEffect e : effectsManager) {
if (e.isEnabled()) {
hasEffects = true;
break;
}
}
return (enabled && !capturing && hasEffects);
}
/** Sets whether or not the post-processor should be enabled */
public void setEnabled (boolean enabled) {
this.enabled = enabled;
}
/** Returns the number of the currently enabled effects */
public int getEnabledEffectsCount () {
return enabledEffects.size;
}
/** Sets the listener that will receive events triggered by the PostProcessor rendering pipeline. */
public void setListener (PostProcessorListener listener) {
this.listener = listener;
}
/** Adds the specified effect to the effect chain and transfer ownership to the PostProcessor, it will manage cleaning it up for
* you. The order of the inserted effects IS important, since effects will be applied in a FIFO fashion, the first added is the
* first being applied. */
public void addEffect (PostProcessorEffect effect) {
effectsManager.add(effect);
}
/** Removes the specified effect from the effect chain. */
public void removeEffect (PostProcessorEffect effect) {
effectsManager.remove(effect);
}
/** Returns the internal framebuffer format, computed from the parameters specified during construction. NOTE: the returned
* Format will be valid after construction and NOT early! */
public static Format getFramebufferFormat () {
return fbFormat;
}
/** Sets the color that will be used to clear the buffer. */
public void setClearColor (Color color) {
clearColor.set(color);
}
/** Sets the color that will be used to clear the buffer. */
public void setClearColor (float r, float g, float b, float a) {
clearColor.set(r, g, b, a);
}
/** Sets the clear bit for when glClear is invoked. */
public void setClearBits (int bits) {
clearBits = bits;
}
/** Sets the depth value with which to clear the depth buffer when needed. */
public void setClearDepth (float depth) {
clearDepth = depth;
}
public void setBufferTextureWrap (TextureWrap u, TextureWrap v) {
compositeWrapU = u;
compositeWrapV = v;
composite.texture1.setWrap(compositeWrapU, compositeWrapV);
composite.texture2.setWrap(compositeWrapU, compositeWrapV);
}
/** Starts capturing the scene, clears the buffer with the clear color specified by {@link #setClearColor(Color)} or
* {@link #setClearColor(float r, float g, float b, float a)}.
*
* @return true or false, whether or not capturing has been initiated. Capturing will fail in case there are no enabled effects
* in the chain or this instance is not enabled or capturing is already started. */
public boolean capture () {
hasCaptured = false;
if (enabled && !capturing) {
if (buildEnabledEffectsList() == 0) {
// no enabled effects
// Gdx.app.log( "PostProcessor::capture()",
// "No post-processor effects enabled" );
return false;
}
capturing = true;
composite.begin();
composite.capture();
if (useDepth) {
Gdx.gl.glClearDepthf(clearDepth);
}
Gdx.gl.glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
Gdx.gl.glClear(clearBits);
return true;
}
return false;
}
/** Starts capturing the scene as {@link #capture()}, but <strong>without</strong> clearing the screen.
*
* @return true or false, whether or not capturing has been initiated. */
public boolean captureNoClear () {
hasCaptured = false;
if (enabled && !capturing) {
if (buildEnabledEffectsList() == 0) {
// no enabled effects
// Gdx.app.log( "PostProcessor::captureNoClear",
// "No post-processor effects enabled" );
return false;
}
capturing = true;
composite.begin();
composite.capture();
return true;
}
return false;
}
/** Stops capturing the scene and returns the result, or null if nothing was captured. */
public FrameBuffer captureEnd () {
if (enabled && capturing) {
capturing = false;
hasCaptured = true;
composite.end();
return composite.getResultBuffer();
}
return null;
}
public PingPongBuffer getCombinedBuffer () {
return composite;
}
/** After a capture/captureEnd action, returns the just captured buffer */
public FrameBuffer captured () {
if (enabled && hasCaptured) {
return composite.getResultBuffer();
}
return null;
}
/** Regenerates and/or rebinds owned resources when needed, eg. when the OpenGL context is lost. */
public void rebind () {
composite.texture1.setWrap(compositeWrapU, compositeWrapV);
composite.texture2.setWrap(compositeWrapU, compositeWrapV);
for (int i = 0; i < buffers.size; i++) {
buffers.get(i).rebind();
}
for (PostProcessorEffect e : effectsManager) {
e.rebind();
}
}
/** Stops capturing the scene and apply the effect chain, if there is one. If the specified output framebuffer is NULL, then the
* rendering will be performed to screen. */
public void render (FrameBuffer dest,boolean draw) {
captureEnd();
if (!hasCaptured) {
return;
}
// Array<PostProcessorEffect> items = manager.items;
Array<PostProcessorEffect> items = enabledEffects;
int count = items.size;
if (count > 0) {
//
Gdx.gl.glDisable(GL20.GL_CULL_FACE);
Gdx.gl.glDisable(GL20.GL_DEPTH_TEST);
// render effects chain, [0,n-1]
if (count > 1) {
for (int i = 0; i < count - 1; i++) {
PostProcessorEffect e = items.get(i);
composite.capture();
{
e.render(composite.getSourceBuffer(), composite.getResultBuffer());
}
}
// complete
composite.end();
}
if (listener != null && dest == null) {
listener.beforeRenderToScreen();
}
// render with null dest (to screen)
if(draw)
items.get(count - 1).render(composite.getResultBuffer(), dest);
// ensure default texture unit #0 is active
Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
} else {
Gdx.app.log("PostProcessor", "No post-processor effects enabled, aborting render");
}
}
public void render (FrameBuffer dest,Class<? extends PostProcessorEffect> c) {
captureEnd();
if (!hasCaptured) {
return;
}
// Array<PostProcessorEffect> items = manager.items;
Array<PostProcessorEffect> items = enabledEffects;
int count = items.size;
if (count > 0) {
//
Gdx.gl.glDisable(GL20.GL_CULL_FACE);
Gdx.gl.glDisable(GL20.GL_DEPTH_TEST);
// render effects chain, [0,n-1]
for(PostProcessorEffect e:items)
if(e.getClass().equals(c)){
composite.capture();
{
e.render(composite.getSourceBuffer(), composite.getResultBuffer());
}
composite.end();
};
if (listener != null && dest == null) {
listener.beforeRenderToScreen();
}
// render with null dest (to screen)
for(PostProcessorEffect e:items)
if(e.getClass().equals(c)){
e.render(composite.getResultBuffer(), dest);
}
// ensure default texture unit #0 is active
Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
} else {
Gdx.app.log("PostProcessor", "No post-processor effects enabled, aborting render");
}
}
/** Convenience method to render to screen. */
public void render (boolean draw) {
render(null,draw);
}
private int buildEnabledEffectsList () {
enabledEffects.clear();
for (PostProcessorEffect e : effectsManager) {
if (e.isEnabled()) {
enabledEffects.add(e);
}
}
return enabledEffects.size;
}
/** Restores the previously set viewport if one was specified earlier and the destination buffer is the screen */
protected static void restoreViewport (FrameBuffer dest) {
if (hasViewport && dest == null) {
Gdx.gl.glViewport((int)viewport.x, (int)viewport.y, (int)viewport.width, (int)viewport.height);
}
}
}